From e2687940915a032aee1734e0925bb9fa450fe96c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:50:59 -0500 Subject: [PATCH 01/99] feat: add stock kimi tool-use eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:添加基于 Kimi Vendor Verifier 原生实现的工具调用评估 --- .github/workflows/benchmark-tmpl.yml | 2 + benchmarks/benchmark_lib.sh | 257 ++++++++- utils/collect_eval_results.py | 4 + utils/evals/EVALS.md | 87 ++- utils/evals/kimi_vendor_eval.py | 255 +++++++++ utils/evals/test_kimi_vendor_eval.py | 287 ++++++++++ utils/evals/test_run_eval_dispatch.py | 506 +++++++++++++++++- utils/evals/thresholds.yaml | 1 + utils/test_collect_eval_results.py | 19 + .../test_validate_reusable_sweep_artifacts.py | 70 ++- utils/validate_reusable_sweep_artifacts.py | 12 + 11 files changed, 1480 insertions(+), 20 deletions(-) create mode 100755 utils/evals/kimi_vendor_eval.py create mode 100644 utils/evals/test_kimi_vendor_eval.py diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 6c4fe50fe5..4dc036a06d 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -392,6 +392,7 @@ jobs: path: | meta_env.json results*.json + kimi_vendor_report.json sample*.jsonl agent_preds.json predictions.jsonl @@ -409,6 +410,7 @@ jobs: rm -f meta_env.json || true # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true + rm -f kimi_vendor_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 1617e1190e..5519682c8f 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -8,6 +8,9 @@ export PYTHONDONTWRITEBYTECODE=1 export PYTHONPYCACHEPREFIX="${PYTHONPYCACHEPREFIX:-/tmp/inferencex-pycache}" mkdir -p "$PYTHONPYCACHEPREFIX" 2>/dev/null || true +INFERENCEX_BENCHMARK_LIB_DIR="$( + cd "$(dirname "${BASH_SOURCE[0]}")" && pwd +)" # Inference server port shared by every benchmark recipe. Launchers that need # a non-default value (e.g. launch_mi355x-amds.sh derives PORT from RUNNER_NAME @@ -816,6 +819,230 @@ _install_lm_eval_deps() { fi } +_require_tool_use_python() { + if python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))'; then + return 0 + fi + + local python_version + python_version="$(python3 -c 'import platform; print(platform.python_version())' 2>/dev/null || printf 'unavailable')" + echo "ERROR: tool-use requires Python >=3.12 (python3 is ${python_version})" >&2 + return 2 +} + +_install_tool_use_eval_deps() { + python3 -m pip install -q --no-cache-dir --break-system-packages \ + "httpx[http2]==0.28.1" \ + "openai==2.14.0" \ + "jsonschema==4.25.1" \ + "pytest==8.4.2" +} + +_kimi_vendor_checkout_is_valid() { + local checkout_dir="$1" + local expected_ref="$2" + local checkout_ref checkout_status tracked_status untracked_files ignored_files + + [ -f "${checkout_dir}/LICENSE" ] \ + && [ -f "${checkout_dir}/pyproject.toml" ] \ + && [ -f "${checkout_dir}/tests/conftest.py" ] \ + && [ -f "${checkout_dir}/tests/__init__.py" ] \ + && [ -f "${checkout_dir}/tests/tool_call_json_schema/conftest.py" ] \ + && [ -f "${checkout_dir}/tests/tool_call_json_schema/validator.py" ] \ + && [ -f "${checkout_dir}/tests/tool_call_json_schema/test_tool_call_json_schema.py" ] \ + && [ -d "${checkout_dir}/testdata/walle_validator_cases/validator_cases" ] \ + || return 1 + checkout_ref="$(git -C "$checkout_dir" rev-parse HEAD 2>/dev/null)" \ + || return 1 + [ "$checkout_ref" = "$expected_ref" ] || return 1 + checkout_status="$( + git -C "$checkout_dir" status --porcelain --untracked-files=all -- \ + LICENSE \ + pyproject.toml \ + tests/conftest.py \ + tests/__init__.py \ + tests/tool_call_json_schema \ + testdata/walle_validator_cases + )" || return 1 + [ -z "$checkout_status" ] || return 1 + tracked_status="$( + git -C "$checkout_dir" status --porcelain --untracked-files=no + )" || return 1 + [ -z "$tracked_status" ] || return 1 + untracked_files="$( + git -C "$checkout_dir" ls-files --others --exclude-standard -- \ + . ':(exclude,top,glob).pytest_cache/**' + )" || return 1 + [ -z "$untracked_files" ] || return 1 + ignored_files="$( + git -C "$checkout_dir" ls-files --others --ignored --exclude-standard -- \ + . ':(exclude,top,glob).pytest_cache/**' + )" || return 1 + [ -z "$ignored_files" ] +} + +_prepare_kimi_vendor_verifier() { + local repo_url="$1" + local verifier_ref="$2" + local checkout_dir + + if [ -n "${KIMI_VENDOR_VERIFIER_DIR:-}" ]; then + checkout_dir="$KIMI_VENDOR_VERIFIER_DIR" + if ! _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then + echo "ERROR: KIMI_VENDOR_VERIFIER_DIR must be at ${verifier_ref}" >&2 + echo "ERROR: required verifier sources must be present and unmodified" >&2 + return 2 + fi + KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" + return 0 + fi + + checkout_dir="/tmp/kimi-vendor-verifier-${verifier_ref}" + if _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then + KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" + return 0 + fi + + command -v git >/dev/null 2>&1 || { + echo "ERROR: git is required to fetch Kimi-Vendor-Verifier" >&2 + return 1 + } + rm -rf "$checkout_dir" + mkdir -p "$(dirname "$checkout_dir")" || return $? + if ! ( + git init -q "$checkout_dir" \ + && git -C "$checkout_dir" remote add origin "$repo_url" \ + && git -C "$checkout_dir" config remote.origin.promisor true \ + && git -C "$checkout_dir" config remote.origin.partialclonefilter blob:none \ + && git -C "$checkout_dir" fetch -q --filter=blob:none --depth=1 \ + origin "$verifier_ref" \ + && git -C "$checkout_dir" update-ref HEAD FETCH_HEAD \ + && git -C "$checkout_dir" sparse-checkout set --no-cone \ + /LICENSE \ + /pyproject.toml \ + /tests/conftest.py \ + /tests/__init__.py \ + /tests/tool_call_json_schema/ \ + /testdata/walle_validator_cases/ \ + && git -C "$checkout_dir" checkout -q --detach HEAD + ); then + rm -rf "$checkout_dir" + echo "ERROR: failed to fetch Kimi-Vendor-Verifier at ${verifier_ref}" >&2 + return 1 + fi + if ! _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then + rm -rf "$checkout_dir" + echo "ERROR: fetched Kimi-Vendor-Verifier checkout is incomplete" >&2 + return 1 + fi + KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" +} + +_write_tool_use_integration_error() { + local adapter_path="$1" + local model_name="$2" + local results_dir="$3" + local message="$4" + + python3 "$adapter_path" \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --integration-error "$message" \ + || true +} + +run_tool_use_eval() { + local port="${PORT:-8888}" + local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}" + local verifier_repo="https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" + local verifier_ref="b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + + while [[ $# -gt 0 ]]; do + case "$1" in + --port|--results-dir) + if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then + echo "ERROR: $1 requires a value" >&2 + return 2 + fi + case "$1" in + --port) port="$2" ;; + --results-dir) results_dir="$2" ;; + esac + shift 2 + ;; + *) + echo "Unknown parameter: $1" >&2 + return 2 + ;; + esac + done + + local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" + if [ "$eval_suite" != "kimi_tool_call_schema" ]; then + echo "ERROR: tool-use supports only EVAL_SUITE=kimi_tool_call_schema" >&2 + export EVAL_RESULT_DIR="" + return 2 + fi + case "${IS_MULTINODE:-false}" in + true|1) + echo "ERROR: tool-use Phase 1 supports single-node evals only" >&2 + export EVAL_RESULT_DIR="" + return 2 + ;; + esac + export EVAL_FRAMEWORK=tool-use + export EVAL_SUITE="$eval_suite" + + local _repo_root + _repo_root="$(cd "$INFERENCEX_BENCHMARK_LIB_DIR/.." && pwd)" + local model_name="${MODEL_NAME:-${MODEL:-}}" + local adapter_path="${_repo_root}/utils/evals/kimi_vendor_eval.py" + + mkdir -p "$results_dir" || return $? + export EVAL_RESULT_DIR="$results_dir" + + local setup_rc integration_error + if _require_tool_use_python; then + : + else + setup_rc=$? + integration_error="tool-use Python version check failed with exit code ${setup_rc}" + echo "ERROR: ${integration_error}" >&2 + _write_tool_use_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" + return "$setup_rc" + fi + if [ "${INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY:-false}" != "true" ]; then + if _install_tool_use_eval_deps; then + export INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY=true + else + setup_rc=$? + integration_error="tool-use dependency installation failed with exit code ${setup_rc}" + echo "ERROR: ${integration_error}" >&2 + _write_tool_use_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" + return "$setup_rc" + fi + fi + if _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref"; then + : + else + setup_rc=$? + integration_error="tool-use verifier checkout failed with exit code ${setup_rc}" + echo "ERROR: ${integration_error}" >&2 + _write_tool_use_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" + return "$setup_rc" + fi + + python3 "$adapter_path" \ + --verifier-dir "$KIMI_VENDOR_VERIFIER_CHECKOUT_DIR" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --api-key EMPTY \ + --model "$model_name" \ + --output-dir "$results_dir" +} + _eval_patches_dir() { cd "$(dirname "${BASH_SOURCE[0]}")/../utils/evals/patches" && pwd } @@ -934,6 +1161,15 @@ run_lm_eval() { tasks_dir="$_repo_root/$tasks_dir" fi + local effective_suite="${EVAL_SUITE:-}" + local task_basename + if [ -z "$effective_suite" ]; then + task_basename="${tasks_dir##*/}" + effective_suite="${task_basename%.yaml}" + effective_suite="${effective_suite%.yml}" + fi + export EVAL_SUITE="$effective_suite" + if [ "${INFERENCEX_LM_EVAL_RUNTIME_READY:-false}" != "true" ]; then _install_lm_eval_deps _patch_lm_eval @@ -1141,12 +1377,22 @@ append_lm_eval_summary() { fi fi fi + local eval_framework="${EVAL_FRAMEWORK:-lm-eval}" + local eval_suite="${EVAL_SUITE:-}" + if [ -z "$eval_suite" ] && [ -n "${EVAL_TASKS_DIR:-}" ]; then + eval_suite="$(basename "${EVAL_TASKS_DIR}")" + eval_suite="${eval_suite%.yaml}" + eval_suite="${eval_suite%.yml}" + fi + eval_suite="${eval_suite:-gsm8k}" cat > "${meta_json}" < /dev/null fi @@ -1635,6 +1887,7 @@ run_eval() { case "$framework" in lm-eval|lm_eval) run_lm_eval "${forwarded[@]}" || eval_rc=$? ;; swebench) run_swebench_eval "${forwarded[@]}" || eval_rc=$? ;; + tool-use) run_tool_use_eval "${forwarded[@]}" || eval_rc=$? ;; *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 667e60bc6f..7bc49c5d8c 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -284,6 +284,10 @@ def build_row(meta: Dict[str, Any], m: Dict[str, Any]) -> Dict[str, Any]: 'source': m.get('source'), } + for metadata_field in ('eval_framework', 'eval_suite'): + if metadata_field in meta: + row[metadata_field] = meta[metadata_field] + # Add universal score field (primary metric for unified comparison) if m.get('strict') is not None: row['score'] = m.get('strict') diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 7320795431..07aa48b4de 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -39,9 +39,85 @@ malformed metadata, duplicates, or raw/aggregate mismatches are not. See [workflow reuse](../../.github/workflows/README.md#reusing-an-approved-pr-full-sweep). ## How? -`run_eval` in `benchmarks/benchmark_lib.sh` runs EleutherAI/lm-evaluation-harness against the server's OpenAI-compatible endpoint. Concurrency is set via `EVAL_CONCURRENT_REQUESTS` env var (not a CLI flag). Results are collected by `utils/collect_eval_results.py` and published as a summary table. +`run_eval` in `benchmarks/benchmark_lib.sh` dispatches to the selected eval +framework against the server's OpenAI-compatible endpoint. The default is +[lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) +(`lm-eval`) with GSM8K. Existing fixed-sequence and agentic paths preserve that +default, and explicit agentic runs can still select SWE-bench. -The default eval framework is [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (`lm-eval`). Agentic eval-only matrix jobs inherit this default and therefore run the same GSM8K task as 8k1k; explicit agentic runs can still select SWE-bench. +The Phase 1 tool-use suite is opt-in. The Kimi-K3 B300 vLLM agentic launcher, +like every existing launcher, continues to select lm-eval/GSM8K by default. To +run the suite after its server is ready, use the existing entrypoint: + +```bash +EVAL_FRAMEWORK=tool-use EVAL_SUITE=kimi_tool_call_schema \ + run_eval --port "$PORT" +``` + +`run_tool_use_eval` supplies `kimi_tool_call_schema` when `EVAL_SUITE` is unset +for a manual `run_eval --framework tool-use` call and rejects every other suite. +The compatibility result continues through the existing collector, suite-aware +artifact identity, and strict `1.0` threshold. +Phase 1 is single-node only and rejects `IS_MULTINODE=true` or `1`; the +multi-node workflow does not yet preserve the stock native report. + +### Stock Kimi tool-call schema smoke + +This suite runs the unmodified +[MoonshotAI/Kimi-Vendor-Verifier](https://github.com/MoonshotAI/Kimi-Vendor-Verifier) +at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Its bundled Walle +schema corpus is sourced from MoonshotAI/walle commit +`cc1c6b7dab5496d5184677ecf4c3b95fc1bd1606` (`v0.1.10`). The upstream +prompt, schema loading and selection, request construction, non-stream and +stream assembly, argument validation, and report generation are all stock. +InferenceX owns only the subprocess invocation and compatibility projection. + +Python 3.12 or newer is required; the runner fails with a version error before +installing or checking out anything on older Python. At runtime it installs only +`httpx[http2]==0.28.1`, `openai==2.14.0`, `jsonschema==4.25.1`, and +`pytest==8.4.2`. It then makes a network checkout from GitHub using a sparse, +detached checkout of the pinned verifier commit containing only: + +- `LICENSE` and `pyproject.toml`; +- `tests/__init__.py`, `tests/conftest.py`, and + `tests/tool_call_json_schema/`; +- `testdata/walle_validator_cases/`. + +An explicitly supplied `KIMI_VENDOR_VERIFIER_DIR` is reused only when it is at +that exact commit, required sources are unmodified, and no extra checkout files +can override the verifier (root `.pytest_cache/` is ignored). The verifier +project and its unrelated benchmark dependencies are not installed. + +The thin `utils/evals/kimi_vendor_eval.py` wrapper runs upstream +`tests/tool_call_json_schema/test_tool_call_json_schema.py` with: + +- base URL `http://127.0.0.1:${PORT}/v1`, API key `EMPTY`, and model + `${MODEL_NAME:-$MODEL}`; +- `--case-dir testdata/walle_validator_cases/validator_cases`, + `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; +- `--tool-json-report /kimi_vendor_report.json`. + +That stock selection chooses Walle case `TestAdditionalProperties:1` and +upstream parametrizes it in both `non-stream` and `stream` modes, for two +results. Requests use upstream's `openai.Client(timeout=120)` unchanged, so +OpenAI SDK 2.14.0's stock retry policy remains in effect, including its default +two retries for eligible connection, timeout, 408, 409, 429, and 5xx failures. +InferenceX does not add request retries or make the two modes concurrent. +`EVAL_CONCURRENT_REQUESTS` remains matrix metadata; multi-value batched +concurrency remains supported only by `lm-eval`. + +The unchanged native `kimi_vendor_report.json` is uploaded alongside the +collector-compatible `results_kimi_vendor_.json`. The +compatibility score is `passed / 2` for task `kimi_tool_call_schema`, primary metric +`exact_match,strict-match`, and effective sample count two. Success requires +pytest to exit zero and exactly two upstream mode results to pass. A setup or +collection failure still produces a zero-score compatibility result with +integration error metadata; the native report can be absent when upstream +cannot collect. + +Phase 1 intentionally covers one stock object-schema case only. It does not +measure broader schema coverage, tool selection among multiple tools, parallel +tool calls, multi-turn tool execution, or general agent quality. ### Benchmark script flow @@ -72,8 +148,11 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: |----------|-------------| | `run_eval` | Unified entrypoint - dispatches to framework-specific runner | | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | +| `run_tool_use_eval` | Runs the pinned stock verifier in non-stream and stream modes | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | +| `_install_tool_use_eval_deps` | Installs the minimal pinned stock-verifier runtime | +| `_prepare_kimi_vendor_verifier` | Prepares or validates the pinned sparse checkout | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | @@ -141,6 +220,8 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `em_flexible` | Flexible extraction (looser number matching) | | `n_eff` | Number of samples evaluated | | `task` | Eval task name (e.g., `gsm8k`) | +| `eval_framework` | Eval runner identity (for example, `lm-eval` or `tool-use`) | +| `eval_suite` | Explicit suite identity used for collection and artifact reuse | ### Environment variables @@ -149,8 +230,10 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `RUN_EVAL` | `false` | Enable eval after throughput benchmark | | `EVAL_ONLY` | `false` | Skip throughput, only run evals (set by workflow) | | `EVAL_FRAMEWORK` | `lm-eval` | Eval framework to use | +| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Eval suite metadata; explicit values take precedence | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | +| `KIMI_VENDOR_VERIFIER_DIR` | generated pinned checkout | Optional pre-existing verifier checkout; exact ref and required paths are validated | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | | `EVAL_CONCURRENT_REQUESTS` | `64` | Concurrent requests during eval; a space-separated list enables sequential batched evals against one live engine | | `EVAL_LIMIT` | empty | Limit eval to first N instances (smoke tests); empty = full set | diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py new file mode 100755 index 0000000000..c01343fe15 --- /dev/null +++ b/utils/evals/kimi_vendor_eval.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Run the stock Kimi Vendor Verifier and project its native report.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from collections.abc import Mapping, Sequence +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +TASK_NAME = "kimi_tool_call_schema" +NATIVE_REPORT_FILENAME = "kimi_vendor_report.json" +COMPATIBILITY_GLOB = "results_kimi_vendor_*.json" +EXPECTED_MODES = {"non-stream", "stream"} + + +def prepare_compatibility_path(output_dir: Path) -> Path: + """Remove stale projections and return a timestamped collector artifact path.""" + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") + return output_dir / f"results_kimi_vendor_{timestamp}.json" + + +def build_pytest_command( + *, base_url: str, api_key: str, model: str, report_path: Path +) -> list[str]: + """Build the fixed Phase 1 invocation of the upstream verifier.""" + return [ + sys.executable, + "-m", + "pytest", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "--base-url", + base_url, + "--api-key", + api_key, + "--smoke-model", + model, + "--think-mode", + "none", + "--selection", + "object", + "--max-cases", + "1", + "--case-dir", + "testdata/walle_validator_cases/validator_cases", + "--max-tokens", + "2048", + "--tool-json-report", + str(report_path), + ] + + +def _mapping(value: Any, name: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise ValueError(f"{name} must be an object") + return value + + +def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: + root = _mapping(report, "report") + summary = _mapping(root.get("summary"), "report.summary") + results = root.get("results") + if not isinstance(results, list): + raise ValueError("report.results must be an array") + + total = summary.get("total") + by_status = _mapping(summary.get("by_status"), "report.summary.by_status") + passed = by_status.get("passed", 0) + if ( + not isinstance(total, int) + or isinstance(total, bool) + or not isinstance(passed, int) + or isinstance(passed, bool) + or passed < 0 + or passed > 2 + ): + raise ValueError("report summary contains invalid counts") + + modes: list[str] = [] + result_passes = 0 + for index, result in enumerate(results): + record = _mapping(result, f"report.results[{index}]") + mode = record.get("mode") + status = record.get("status") + if not isinstance(mode, str) or not isinstance(status, str): + raise ValueError(f"report.results[{index}] has invalid mode or status") + modes.append(mode) + result_passes += status == "passed" + + if total != len(results) or passed != result_passes: + raise ValueError("report summary does not match result records") + + score = passed / 2.0 + compatibility = _compatibility_result(model, score) + complete_pass = ( + total == 2 + and passed == 2 + and len(results) == 2 + and set(modes) == EXPECTED_MODES + and len(modes) == len(set(modes)) + ) + return compatibility, complete_pass + + +def _compatibility_result( + model: str, score: float, integration_error: BaseException | None = None +) -> dict[str, Any]: + result: dict[str, Any] = { + "lm_eval_version": "kimi-vendor-verifier", + "model_name": model, + "model_args": f"pretrained={model}", + "results": { + TASK_NAME: { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.0, + } + }, + "configs": { + TASK_NAME: { + "task": TASK_NAME, + "output_type": "generate_until", + "num_fewshot": 0, + "repeats": 1, + "metric_list": [ + { + "metric": "exact_match", + "aggregation": "mean", + "higher_is_better": True, + } + ], + "filter_list": [ + {"name": "strict-match", "filter": [{"function": "identity"}]} + ], + } + }, + "versions": {TASK_NAME: 1}, + "n-shot": {TASK_NAME: 0}, + "higher_is_better": {TASK_NAME: {"exact_match": True}}, + "n-samples": {TASK_NAME: {"original": 2, "effective": 2}}, + } + if integration_error is not None: + result["integration_error"] = { + "type": type(integration_error).__name__, + "message": str(integration_error), + } + return result + + +def _write_compatibility(path: Path, result: Mapping[str, Any]) -> None: + path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + +def run_evaluation( + *, + verifier_dir: Path, + base_url: str, + api_key: str, + model: str, + output_dir: Path, +) -> bool: + """Run upstream pytest and always attempt to publish a compatibility result.""" + output_dir.mkdir(parents=True, exist_ok=True) + native_report = output_dir / NATIVE_REPORT_FILENAME + compatibility_path = prepare_compatibility_path(output_dir) + subprocess_rc: int | None = None + integration_error: BaseException | None = None + compatibility = _compatibility_result(model, 0.0) + complete_pass = False + + try: + native_report.unlink(missing_ok=True) + completed = subprocess.run( + build_pytest_command( + base_url=base_url, + api_key=api_key, + model=model, + report_path=native_report.resolve(), + ), + cwd=verifier_dir, + check=False, + ) + subprocess_rc = completed.returncode + report = json.loads(native_report.read_text(encoding="utf-8")) + compatibility, complete_pass = _project_report(model, report) + except (OSError, ValueError, json.JSONDecodeError) as exc: + integration_error = exc + compatibility = _compatibility_result(model, 0.0, exc) + finally: + try: + _write_compatibility(compatibility_path, compatibility) + except OSError as exc: + if integration_error is not None: + exc.add_note(f"Earlier integration error: {integration_error}") + raise + + return subprocess_rc == 0 and complete_pass and integration_error is None + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run the pinned stock Kimi Vendor Verifier tool-schema smoke test." + ) + parser.add_argument("--verifier-dir", type=Path) + parser.add_argument("--base-url") + parser.add_argument("--api-key", default="EMPTY") + parser.add_argument("--model", required=True) + parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument("--integration-error") + args = parser.parse_args(argv) + if args.integration_error is None: + missing = [ + option + for option, value in ( + ("--verifier-dir", args.verifier_dir), + ("--base-url", args.base_url), + ) + if value is None + ] + if missing: + parser.error( + f"{', '.join(missing)} required unless --integration-error is provided" + ) + return args + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + if args.integration_error is not None: + args.output_dir.mkdir(parents=True, exist_ok=True) + (args.output_dir / NATIVE_REPORT_FILENAME).unlink(missing_ok=True) + _write_compatibility( + prepare_compatibility_path(args.output_dir), + _compatibility_result( + args.model, 0.0, RuntimeError(args.integration_error) + ), + ) + return 1 + passed = run_evaluation( + verifier_dir=args.verifier_dir, + base_url=args.base_url, + api_key=args.api_key, + model=args.model, + output_dir=args.output_dir, + ) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py new file mode 100644 index 0000000000..bfeb49819d --- /dev/null +++ b/utils/evals/test_kimi_vendor_eval.py @@ -0,0 +1,287 @@ +import json +import re +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import kimi_vendor_eval as kve + + +def _native_report(*, stream_status: str = "passed") -> dict: + statuses = ["passed", stream_status] + by_status: dict[str, int] = {} + for status in statuses: + by_status[status] = by_status.get(status, 0) + 1 + return { + "summary": { + "total": 2, + "by_status": by_status, + "by_selection_reason": {"object_schema": 2}, + "by_mode": { + "non-stream": {"passed": 1}, + "stream": {stream_status: 1}, + }, + }, + "results": [ + {"mode": "non-stream", "status": "passed"}, + {"mode": "stream", "status": stream_status}, + ], + } + + +def _compatibility_file(output_dir: Path) -> Path: + matches = list(output_dir.glob(kve.COMPATIBILITY_GLOB)) + assert len(matches) == 1 + assert re.fullmatch( + r"results_kimi_vendor_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", + matches[0].name, + ) + return matches[0] + + +def _projected(output_dir: Path) -> dict: + return json.loads(_compatibility_file(output_dir).read_text(encoding="utf-8")) + + +def _score(output_dir: Path) -> float: + return _projected(output_dir)["results"][kve.TASK_NAME][ + "exact_match,strict-match" + ] + + +def test_builds_exact_upstream_pytest_command(tmp_path: Path) -> None: + report = tmp_path / kve.NATIVE_REPORT_FILENAME + + command = kve.build_pytest_command( + base_url="http://127.0.0.1:8000/v1", + api_key="EMPTY", + model="test-model", + report_path=report, + ) + + assert command == [ + sys.executable, + "-m", + "pytest", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "--base-url", + "http://127.0.0.1:8000/v1", + "--api-key", + "EMPTY", + "--smoke-model", + "test-model", + "--think-mode", + "none", + "--selection", + "object", + "--max-cases", + "1", + "--case-dir", + "testdata/walle_validator_cases/validator_cases", + "--max-tokens", + "2048", + "--tool-json-report", + str(report), + ] + + +def test_full_pass_projects_score_and_preserves_native_report( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + verifier_dir = tmp_path / "verifier" + verifier_dir.mkdir() + output_dir = tmp_path / "output" + output_dir.mkdir() + (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( + "stale", encoding="utf-8" + ) + native_bytes = (json.dumps(_native_report(), indent=2) + "\n").encode() + invocation: dict[str, object] = {} + + def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: + invocation.update(command=command, cwd=cwd, check=check) + output_dir.mkdir(parents=True, exist_ok=True) + (output_dir / kve.NATIVE_REPORT_FILENAME).write_bytes(native_bytes) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + + passed = kve.run_evaluation( + verifier_dir=verifier_dir, + base_url="http://localhost:8000/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + assert passed + assert invocation["cwd"] == verifier_dir + assert invocation["check"] is False + assert invocation["command"] == kve.build_pytest_command( + base_url="http://localhost:8000/v1", + api_key="EMPTY", + model="model-a", + report_path=(output_dir / kve.NATIVE_REPORT_FILENAME).resolve(), + ) + assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes + projected = _projected(output_dir) + assert _score(output_dir) == 1.0 + assert projected["n-samples"][kve.TASK_NAME] == {"original": 2, "effective": 2} + assert "integration_error" not in projected + + +def test_one_mode_failure_projects_partial_score_and_fails( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + output_dir = tmp_path / "output" + + def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: + report_path = Path(command[command.index("--tool-json-report") + 1]) + report_path.write_text(json.dumps(_native_report(stream_status="failed"))) + return SimpleNamespace(returncode=1) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + + passed = kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + assert not passed + assert _score(output_dir) == 0.5 + + +@pytest.mark.parametrize("native_contents", [None, "{not-json"]) +def test_missing_or_malformed_report_writes_zero_score_with_error( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + native_contents: str | None, +) -> None: + output_dir = tmp_path / "output" + + def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: + if native_contents is not None: + report_path = Path(command[command.index("--tool-json-report") + 1]) + report_path.write_text(native_contents, encoding="utf-8") + return SimpleNamespace(returncode=1) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + + passed = kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + projected = _projected(output_dir) + assert not passed + assert _score(output_dir) == 0.0 + assert projected["integration_error"]["type"] in { + "FileNotFoundError", + "JSONDecodeError", + } + assert projected["integration_error"]["message"] + + +def test_collection_failure_cannot_project_a_stale_passing_report( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + output_dir = tmp_path / "output" + output_dir.mkdir() + native_report = output_dir / kve.NATIVE_REPORT_FILENAME + native_report.write_text(json.dumps(_native_report()), encoding="utf-8") + + def collection_failure( + command: list[str], *, cwd: Path, check: bool + ) -> SimpleNamespace: + assert not native_report.exists() + return SimpleNamespace(returncode=2) + + monkeypatch.setattr(kve.subprocess, "run", collection_failure) + + passed = kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + projected = _projected(output_dir) + assert not passed + assert _score(output_dir) == 0.0 + assert projected["integration_error"]["type"] == "FileNotFoundError" + + +def test_subprocess_launch_failure_writes_zero_score_with_error( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + def fail_to_launch(*args: object, **kwargs: object) -> subprocess.CompletedProcess: + raise OSError("pytest could not launch") + + monkeypatch.setattr(kve.subprocess, "run", fail_to_launch) + output_dir = tmp_path / "output" + + passed = kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + projected = _projected(output_dir) + assert not passed + assert _score(output_dir) == 0.0 + assert projected["integration_error"] == { + "type": "OSError", + "message": "pytest could not launch", + } + + + +def test_cli_integration_error_writes_failure_without_running_pytest( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + def unexpected_run(*args: object, **kwargs: object) -> None: + pytest.fail("integration-error mode must not launch pytest") + + monkeypatch.setattr(kve.subprocess, "run", unexpected_run) + output_dir = tmp_path / "output" + output_dir.mkdir() + (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( + "stale", encoding="utf-8" + ) + (output_dir / kve.NATIVE_REPORT_FILENAME).write_text( + json.dumps(_native_report()), encoding="utf-8" + ) + + return_code = kve.main( + [ + "--model", + "model-a", + "--output-dir", + str(output_dir), + "--integration-error", + "pinned verifier checkout failed", + ] + ) + + projected = _projected(output_dir) + assert return_code == 1 + assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() + assert _score(output_dir) == 0.0 + assert projected["integration_error"] == { + "type": "RuntimeError", + "message": "pinned verifier checkout failed", + } \ No newline at end of file diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 45be3a6e4d..067f2165e0 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1,25 +1,36 @@ from __future__ import annotations +import json import os import stat import subprocess from pathlib import Path +import pytest + BENCHMARK_LIB = Path(__file__).resolve().parents[2] / "benchmarks" / "benchmark_lib.sh" _SCRIPT = r''' source "$BENCHMARK_LIB" run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } -append_lm_eval_summary() { echo "STAGED=summary"; } +run_tool_use_eval() { echo "DISPATCH=tool-use"; } +append_lm_eval_summary() { echo "STAGED=summary FRAMEWORK=$EVAL_FRAMEWORK"; } export EVAL_MAX_MODEL_LEN=16384 -unset EVAL_CONCURRENT_REQUESTS +export EVAL_CONCURRENT_REQUESTS="${REQUESTED_CONC:-}" run_eval ${CLI_FW:+--framework "$CLI_FW"} --port 8888 ''' -def _dispatch(*, is_agentic: str = "0", eval_only: str = "false", cli_fw=None, env_fw=None) -> str: +def _dispatch( + *, + is_agentic: str = "0", + eval_only: str = "false", + cli_fw=None, + env_fw=None, + requested_conc=None, +) -> str: env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -29,11 +40,14 @@ def _dispatch(*, is_agentic: str = "0", eval_only: str = "false", cli_fw=None, e } env.pop("EVAL_FRAMEWORK", None) env.pop("CLI_FW", None) + env.pop("REQUESTED_CONC", None) env.pop("KV_OFFLOAD_BACKEND", None) if cli_fw is not None: env["CLI_FW"] = cli_fw if env_fw is not None: env["EVAL_FRAMEWORK"] = env_fw + if requested_conc is not None: + env["REQUESTED_CONC"] = str(requested_conc) res = subprocess.run( ["bash", "-c", _SCRIPT], env=env, text=True, capture_output=True, check=True ) @@ -52,6 +66,7 @@ def test_agentic_eval_only_stages_summary(): output = _dispatch(is_agentic="1", eval_only="true") assert "DISPATCH=lm-eval" in output assert "STAGED=summary" in output + assert "FRAMEWORK=lm-eval" in output def test_fixed_seqlen_eval_only_leaves_staging_to_recipe(): @@ -71,10 +86,73 @@ def test_env_can_force_swebench_on_fixed_seqlen(): assert "DISPATCH=swebench" in _dispatch(is_agentic="0", env_fw="swebench") +def test_cli_swebench_framework_is_canonical_in_metadata() -> None: + output = _dispatch( + is_agentic="1", + eval_only="true", + cli_fw="swebench", + ) + assert "DISPATCH=swebench" in output + assert "FRAMEWORK=swebench" in output + + +def test_env_can_force_tool_use_on_agentic_eval() -> None: + output = _dispatch( + is_agentic="1", + eval_only="true", + env_fw="tool-use", + ) + assert "DISPATCH=tool-use" in output + assert "FRAMEWORK=tool-use" in output + + +def test_tool_use_skips_unused_model_context_loading() -> None: + script = r''' +source "$BENCHMARK_LIB" +unset EVAL_MAX_MODEL_LEN +compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } +run_tool_use_eval() { echo "DISPATCH=tool-use"; } +export EVAL_FRAMEWORK=tool-use +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +run_eval --port 8888 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "DISPATCH=tool-use" in result.stdout + assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout + + +def test_tool_use_accepts_single_matrix_concurrency_identity() -> None: + assert "DISPATCH=tool-use" in _dispatch( + is_agentic="1", + env_fw="tool-use", + requested_conc=64, + ) + + def test_recipe_lm_eval_arg_still_lm_eval_on_fixed_seqlen(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="0", cli_fw="lm-eval") +def test_lm_eval_alias_is_canonicalized_in_metadata() -> None: + output = _dispatch( + is_agentic="1", + eval_only="true", + cli_fw="lm_eval", + ) + assert "DISPATCH=lm-eval" in output + assert "FRAMEWORK=lm-eval" in output + + def _run_invalid_call(call: str) -> subprocess.CompletedProcess: env = { **os.environ, @@ -95,6 +173,298 @@ def test_run_eval_rejects_missing_framework_value(): assert "--framework requires a value" in result.stderr +def test_tool_use_rejects_batched_concurrency() -> None: + result = _run_invalid_call( + "EVAL_MAX_MODEL_LEN=16384 " + "EVAL_CONCURRENT_REQUESTS='1 4' " + "run_eval --framework tool-use" + ) + assert result.returncode == 1 + assert "batched eval concurrency is only supported for lm-eval" in result.stderr + + +def test_tool_use_rejects_unsupported_suite() -> None: + result = _run_invalid_call( + "EVAL_SUITE=gsm8k run_tool_use_eval" + ) + assert result.returncode == 2 + assert "supports only EVAL_SUITE=kimi_tool_call_schema" in result.stderr + + +def test_tool_use_rejects_multinode() -> None: + for value in ("true", "1"): + result = _run_invalid_call( + f"EVAL_SUITE=kimi_tool_call_schema IS_MULTINODE={value} " + "run_tool_use_eval" + ) + assert result.returncode == 2 + assert "supports single-node evals only" in result.stderr + + +@pytest.mark.parametrize( + ("failure_stage", "failure_rc", "message"), + ( + ("python", 11, "tool-use Python version check failed"), + ("dependencies", 12, "tool-use dependency installation failed"), + ("checkout", 13, "tool-use verifier checkout failed"), + ), +) +def test_tool_use_setup_failure_writes_compatibility_result( + tmp_path: Path, + failure_stage: str, + failure_rc: int, + message: str, +) -> None: + results_dir = tmp_path / "results" + script = r''' +source "$BENCHMARK_LIB" +_require_tool_use_python() { + [ "$FAILURE_STAGE" = python ] && return "$FAILURE_RC" + return 0 +} +_install_tool_use_eval_deps() { + [ "$FAILURE_STAGE" = dependencies ] && return "$FAILURE_RC" + return 0 +} +_prepare_kimi_vendor_verifier() { + [ "$FAILURE_STAGE" = checkout ] && return "$FAILURE_RC" + KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$FAKE_VERIFIER_DIR" + return 0 +} +run_tool_use_eval --results-dir "$RESULTS_DIR" +printf 'SETUP_RC=%s\n' "$?" +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "FAKE_VERIFIER_DIR": str(tmp_path / "verifier"), + "FAILURE_STAGE": failure_stage, + "FAILURE_RC": str(failure_rc), + "MODEL": "test-model", + "IS_MULTINODE": "false", + "KV_OFFLOADING": "none", + } + for key in ( + "EVAL_FRAMEWORK", + "EVAL_SUITE", + "EVAL_RESULT_DIR", + "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", + "KIMI_VENDOR_VERIFIER_DIR", + "KIMI_VENDOR_VERIFIER_CHECKOUT_DIR", + "MODEL_NAME", + ): + env.pop(key, None) + + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + expected_message = f"{message} with exit code {failure_rc}" + + assert f"SETUP_RC={failure_rc}" in result.stdout + assert expected_message in result.stderr + score_files = list(results_dir.glob("results*.json")) + assert len(score_files) == 1 + score_result = json.loads(score_files[0].read_text()) + assert ( + score_result["results"]["kimi_tool_call_schema"][ + "exact_match,strict-match" + ] + == 0.0 + ) + assert score_result["integration_error"]["message"] == expected_message + assert not (results_dir / "kimi_vendor_report.json").exists() + + +def test_kimi_vendor_checkout_rejects_source_changes(tmp_path: Path) -> None: + checkout = tmp_path / "verifier" + required_files = ( + "LICENSE", + "pyproject.toml", + "tests/conftest.py", + "tests/__init__.py", + "tests/tool_call_json_schema/conftest.py", + "tests/tool_call_json_schema/validator.py", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "testdata/walle_validator_cases/validator_cases/case.jsonl", + ) + for relative_path in required_files: + path = checkout / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(f"{relative_path}\n") + + subprocess.run(["git", "init", "-q", str(checkout)], check=True) + subprocess.run(["git", "-C", str(checkout), "add", "."], check=True) + subprocess.run( + [ + "git", + "-C", + str(checkout), + "-c", + "user.name=InferenceX Tests", + "-c", + "user.email=tests@inferencex.invalid", + "commit", + "-qm", + "fixture", + ], + check=True, + ) + verifier_ref = subprocess.run( + ["git", "-C", str(checkout), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + def checkout_is_valid() -> bool: + result = subprocess.run( + [ + "bash", + "-c", + 'source "$BENCHMARK_LIB"; ' + '_kimi_vendor_checkout_is_valid "$CHECKOUT" "$VERIFIER_REF"', + ], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "CHECKOUT": str(checkout), + "VERIFIER_REF": verifier_ref, + "KV_OFFLOADING": "none", + }, + ) + return result.returncode == 0 + + assert checkout_is_valid() + pytest_cache = checkout / ".pytest_cache" / "v" / "cache" / "nodeids" + pytest_cache.parent.mkdir(parents=True) + pytest_cache.write_text("[]\n") + assert checkout_is_valid() + + with (checkout / ".git/info/exclude").open("a") as exclude_file: + exclude_file.write("\n/conftest.py\n") + root_override = checkout / "conftest.py" + root_override.write_text("# ignored root override\n") + assert not checkout_is_valid() + root_override.unlink() + + root_override.write_text("# staged root override\n") + subprocess.run( + ["git", "-C", str(checkout), "add", "-f", "conftest.py"], + check=True, + ) + assert not checkout_is_valid() + subprocess.run( + ["git", "-C", str(checkout), "reset", "-q", "HEAD", "--", "conftest.py"], + check=True, + ) + root_override.unlink() + + override = checkout / "tests/tool_call_json_schema/local_override.py" + override.write_text("# untracked override\n") + assert not checkout_is_valid() + override.unlink() + + validator = checkout / "tests/tool_call_json_schema/validator.py" + validator.write_text("# modified verifier\n") + assert not checkout_is_valid() + + +def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: + shim_dir = tmp_path / "bin" + shim_dir.mkdir() + python_shim = shim_dir / "python3" + python_shim.write_text( + "#!/usr/bin/env bash\n" + "printf 'PYTHON_ARG=<%s>\\n' \"$@\"\n" + ) + python_shim.chmod( + python_shim.stat().st_mode | stat.S_IXUSR + ) + results_dir = tmp_path / "results" + verifier_dir = tmp_path / "verifier" + script = r''' +source "$BENCHMARK_LIB" +_require_tool_use_python() { echo "PYTHON_VERSION=OK"; } +_install_tool_use_eval_deps() { echo "INSTALL=UPSTREAM_MINIMAL"; } +_prepare_kimi_vendor_verifier() { + printf 'CHECKOUT_REPO=%s\n' "$1" + printf 'CHECKOUT_REF=%s\n' "$2" + KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$VERIFIER_DIR" +} +run_tool_use_eval --port 9999 --results-dir "$RESULTS_DIR" +printf 'EVAL_FRAMEWORK=%s\n' "$EVAL_FRAMEWORK" +printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" +printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" +printf 'RUNTIME_READY=%s\n' "$INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY" +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "VERIFIER_DIR": str(verifier_dir), + "MODEL": "test-model", + "OPENAI_API_KEY": "must-not-be-forwarded", + "PATH": f"{shim_dir}:{os.environ['PATH']}", + "KV_OFFLOADING": "none", + "IS_MULTINODE": "false", + } + for key in ( + "EVAL_FRAMEWORK", + "EVAL_SUITE", + "EVAL_RESULT_DIR", + "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", + "KIMI_VENDOR_VERIFIER_DIR", + "KIMI_VENDOR_VERIFIER_CHECKOUT_DIR", + "MODEL_NAME", + ): + env.pop(key, None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + output = result.stdout + expected_adapter = ( + BENCHMARK_LIB.parents[1] / "utils/evals/kimi_vendor_eval.py" + ) + + assert "PYTHON_VERSION=OK" in output + assert output.count("INSTALL=UPSTREAM_MINIMAL") == 1 + assert ( + "CHECKOUT_REPO=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" + in output + ) + assert ( + "CHECKOUT_REF=b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + in output + ) + assert f"PYTHON_ARG=<{expected_adapter}>" in output + for flag in ( + "--verifier-dir", + "--base-url", + "--api-key", + "--model", + "--output-dir", + ): + assert f"PYTHON_ARG=<{flag}>" in output + assert f"PYTHON_ARG=<{verifier_dir}>" in output + assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=" in output + assert f"PYTHON_ARG=<{results_dir}>" in output + assert "EVAL_FRAMEWORK=tool-use" in output + assert "EVAL_SUITE=kimi_tool_call_schema" in output + assert f"EVAL_RESULT_DIR={results_dir}" in output + assert "RUNTIME_READY=true" in output + + def test_run_lm_eval_rejects_missing_option_value(): result = _run_invalid_call("run_lm_eval --port") assert result.returncode == 2 @@ -177,6 +547,94 @@ def test_lm_eval_defaults_to_gsm8k(): assert "utils/evals/gsm8k.yaml" in out +def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: + work_dir = tmp_path / "work" + results_dir = tmp_path / "results" + work_dir.mkdir(parents=True) + results_dir.mkdir() + script = r''' +source "$BENCHMARK_LIB" +cd "$WORK_DIR" +append_lm_eval_summary >/dev/null +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "WORK_DIR": str(work_dir), + "EVAL_RESULT_DIR": str(results_dir), + "MODEL": "test-model", + "CONC": "7", + "KV_OFFLOADING": "none", + } + for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_TASKS_DIR"): + env.pop(key, None) + env.update(overrides) + subprocess.run(["bash", "-c", script], env=env, check=True) + return json.loads((work_dir / "meta_env.json").read_text()) + + +def test_summary_metadata_preserves_lm_eval_gsm8k_defaults(tmp_path: Path) -> None: + meta = _summary_metadata(tmp_path) + + assert meta["eval_framework"] == "lm-eval" + assert meta["eval_suite"] == "gsm8k" + assert meta["conc"] == 7 + + +def test_run_lm_eval_exports_cli_task_suite_to_metadata(tmp_path: Path) -> None: + work_dir = tmp_path / "work" + results_dir = tmp_path / "results" + work_dir.mkdir() + results_dir.mkdir() + script = r''' +set -e +source "$BENCHMARK_LIB" +cd "$WORK_DIR" +python3() { :; } +export EVAL_MAX_MODEL_LEN=16384 +export INFERENCEX_LM_EVAL_RUNTIME_READY=true +run_lm_eval --task custom.yaml --results-dir "$RESULTS_DIR" +append_lm_eval_summary >/dev/null +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "WORK_DIR": str(work_dir), + "RESULTS_DIR": str(results_dir), + "MODEL": "test-model", + "MODEL_NAME": "test-model", + "OPENAI_API_KEY": "EMPTY", + "KV_OFFLOADING": "none", + } + for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_TASKS_DIR"): + env.pop(key, None) + + subprocess.run(["bash", "-c", script], env=env, check=True) + meta = json.loads((work_dir / "meta_env.json").read_text()) + + assert meta["eval_framework"] == "lm-eval" + assert meta["eval_suite"] == "custom" + + +def test_summary_metadata_prefers_explicit_suite_then_task_basename( + tmp_path: Path, +) -> None: + from_task = _summary_metadata( + tmp_path / "task", + EVAL_TASKS_DIR="/tmp/custom_reasoning.yaml", + ) + explicit = _summary_metadata( + tmp_path / "explicit", + EVAL_FRAMEWORK="tool-use", + EVAL_SUITE="kimi_tool_call_schema", + EVAL_TASKS_DIR="/tmp/ignored.yaml", + ) + + assert from_task["eval_suite"] == "custom_reasoning" + assert explicit["eval_framework"] == "tool-use" + assert explicit["eval_suite"] == "kimi_tool_call_schema" + + _MODAL_CREDS_SCRIPT = r''' source "$BENCHMARK_LIB" @@ -552,14 +1010,28 @@ def test_agentic_eval_limit_full_runs_whole_split(tmp_path): source "$BENCHMARK_LIB" 2>/dev/null _install_swebench_agent_deps() { :; } _ensure_modal_credentials() { :; } -_run_swebench_agentic_generation() { echo "GEN=agentic"; return 42; } -run_lm_eval() { echo "GEN=single-shot"; return 42; } +_run_swebench_agentic_generation() { + echo "GEN=agentic" + echo "SUITE=$EVAL_SUITE" + return 42 +} +run_lm_eval() { + echo "GEN=single-shot" + echo "SUITE=$EVAL_SUITE" + return 42 +} run_swebench_eval --port 8888 echo "RC=$?" ''' -def _gen_mode(tmp_path, *, is_agentic, gen_mode=None) -> str: +def _gen_mode( + tmp_path: Path, + *, + is_agentic, + gen_mode=None, + eval_suite=None, +) -> str: env = {**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "KV_OFFLOADING": "none", @@ -567,8 +1039,11 @@ def _gen_mode(tmp_path, *, is_agentic, gen_mode=None) -> str: "EVAL_RESULT_DIR": str(tmp_path / "out")} env.pop("SWEBENCH_GEN_MODE", None) env.pop("SCENARIO_TYPE", None) + env.pop("EVAL_SUITE", None) if gen_mode is not None: env["SWEBENCH_GEN_MODE"] = gen_mode + if eval_suite is not None: + env["EVAL_SUITE"] = eval_suite res = subprocess.run(["bash", "-c", _GENMODE_SCRIPT], env=env, text=True, capture_output=True, cwd=BENCHMARK_LIB.parents[1]) @@ -577,7 +1052,9 @@ def _gen_mode(tmp_path, *, is_agentic, gen_mode=None) -> str: def test_gen_mode_defaults_to_agentic(tmp_path): - assert "GEN=agentic" in _gen_mode(tmp_path, is_agentic="1") + output = _gen_mode(tmp_path, is_agentic="1") + assert "GEN=agentic" in output + assert "SUITE=swebench_lite" in output def test_gen_mode_agentic_even_without_agentic_scenario(tmp_path): @@ -585,7 +1062,20 @@ def test_gen_mode_agentic_even_without_agentic_scenario(tmp_path): def test_explicit_single_shot_escape_hatch(tmp_path): - assert "GEN=single-shot" in _gen_mode(tmp_path, is_agentic="1", gen_mode="single-shot") + output = _gen_mode(tmp_path, is_agentic="1", gen_mode="single-shot") + assert "GEN=single-shot" in output + assert "SUITE=swebench_lite" in output + + +def test_swebench_generation_modes_preserve_explicit_suite(tmp_path): + for gen_mode in ("agentic", "single-shot"): + output = _gen_mode( + tmp_path / gen_mode, + is_agentic="1", + gen_mode=gen_mode, + eval_suite="explicit_swebench", + ) + assert "SUITE=explicit_swebench" in output def test_agent_sandbox_cpu_knob(tmp_path): diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index 6ff9731c45..bb3a4f58b6 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -1,6 +1,7 @@ # Model thresholds override task defaults. default: gsm8k: 0.90 + kimi_tool_call_schema: 1.0 gpqa_diamond_cot_n_shot: 0.30 swebench_lite: 0.50 models: diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 019bbdf123..200be70e64 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -21,6 +21,21 @@ def test_build_row_preserves_sequence_lengths() -> None: assert row["isl"] == 1024 assert row["osl"] == 1024 + assert "eval_framework" not in row + assert "eval_suite" not in row + + +def test_build_row_preserves_explicit_eval_metadata() -> None: + row = build_row( + { + "eval_framework": "tool-use", + "eval_suite": "kimi_tool_call_schema", + }, + {"task": "kimi_tool_call_schema"}, + ) + + assert row["eval_framework"] == "tool-use" + assert row["eval_suite"] == "kimi_tool_call_schema" def _write_lm_eval_result(path: Path, score: float) -> None: @@ -67,6 +82,8 @@ def test_collect_eval_rows_expands_batched_concurrencies( "completed_eval_concs": [4, 16], "failed_eval_concs": [], "conc": 4, + "eval_framework": "lm-eval", + "eval_suite": "gsm8k", })) _write_lm_eval_result( artifact_dir / "results_test_conc4.json", @@ -81,6 +98,8 @@ def test_collect_eval_rows_expands_batched_concurrencies( assert [row["conc"] for row in rows] == [4, 16] assert [row["score"] for row in rows] == [0.90, 0.91] + assert {row["eval_framework"] for row in rows} == {"lm-eval"} + assert {row["eval_suite"] for row in rows} == {"gsm8k"} def test_collect_eval_rows_ignores_failed_batch_points( diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index 69eb633cdc..285bb6fcb3 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -32,8 +32,9 @@ def single_eval_result( runner: str = "h100-dgxc-slurm", isl: int = 8192, osl: int = 1024, + eval_suite: str | None = None, ) -> dict: - return { + row = { "is_multinode": False, "hw": runner.upper(), "model_prefix": "gptoss", @@ -51,6 +52,9 @@ def single_eval_result( "conc": conc, "task": "gsm8k", } + if eval_suite is not None: + row["eval_suite"] = eval_suite + return row def single_eval_meta( @@ -58,8 +62,9 @@ def single_eval_meta( runner: str = "h100-dgxc-slurm", isl: int = 8192, osl: int = 1024, + eval_suite: str | None = None, ) -> dict: - row = single_eval_result(conc, runner, isl, osl) + row = single_eval_result(conc, runner, isl, osl, eval_suite) row["infmax_model_prefix"] = row.pop("model_prefix") return row @@ -72,11 +77,20 @@ def write_raw_eval_artifact( physical_runner: str = "h100-dgxc-slurm_00", isl: int = 8192, osl: int = 1024, + eval_suite: str | None = None, ) -> None: artifact_dir = root / f"eval_result_conc{conc}_{physical_runner}" artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text( - json.dumps(single_eval_meta(conc, logical_runner, isl, osl)) + json.dumps( + single_eval_meta( + conc, + logical_runner, + isl, + osl, + eval_suite, + ) + ) ) @@ -330,7 +344,7 @@ def test_eval_validation_requires_raw_result_dirs_not_eval_debug_dirs( assert any("unexpected" in error for error in errors) -def test_eval_validation_accepts_matching_raw_and_aggregate( +def test_eval_validation_accepts_matching_legacy_artifacts_without_suite( tmp_path: Path, ) -> None: write_eval_aggregate( @@ -347,6 +361,31 @@ def test_eval_validation_accepts_matching_raw_and_aggregate( assert validate_eval_artifacts(tmp_path) == [] +def test_eval_validation_separates_explicit_suite_identities( + tmp_path: Path, +) -> None: + gsm8k = single_eval_result(32, eval_suite="gsm8k") + tool_use = single_eval_result( + 32, + eval_suite="kimi_tool_call_schema", + ) + write_eval_aggregate(tmp_path, [gsm8k, tool_use]) + write_raw_eval_artifact( + tmp_path, + 32, + eval_suite="gsm8k", + ) + write_raw_eval_artifact( + tmp_path, + 32, + physical_runner="h100-dgxc-slurm_01", + eval_suite="kimi_tool_call_schema", + ) + + assert eval_key(gsm8k) != eval_key(tool_use) + assert validate_eval_artifacts(tmp_path) == [] + + def test_eval_validation_distinguishes_sequence_lengths(tmp_path: Path) -> None: write_eval_aggregate( tmp_path, @@ -634,18 +673,23 @@ def _dd_write_aggregate(root: Path, rows: list[dict]) -> Path: def _dd_write_legacy_raw( - root: Path, name: str, conc: int, timestamp: str | None + root: Path, + name: str, + conc: int, + timestamp: str | None, + result_prefix: str = "results_", ) -> None: artifact_dir = root / name artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text(json.dumps(_dd_meta(conc))) if timestamp is not None: - (artifact_dir / f"results_{timestamp}.json").write_text("{}") + (artifact_dir / f"{result_prefix}{timestamp}.json").write_text("{}") def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: # Three reruns of one eval plus a result-less attempt, mirroring a flaky # config retried until it passed. + # The latest rerun uses the tool-use adapter's timestamped result prefix. old, mid, new, empty = ( "eval_minimaxm3_conc4096_b300-nv_15", "eval_minimaxm3_conc4096_b300-nv_16", @@ -654,13 +698,23 @@ def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: ) _dd_write_legacy_raw(tmp_path, old, 4096, "2026-06-26T13-00-22.596040") _dd_write_legacy_raw(tmp_path, mid, 4096, "2026-06-26T19-00-52.356121") - _dd_write_legacy_raw(tmp_path, new, 4096, "2026-06-27T04-28-31.838775") + _dd_write_legacy_raw( + tmp_path, + new, + 4096, + "2026-06-27T04-28-31.838775", + result_prefix="results_kimi_vendor_", + ) _dd_write_legacy_raw(tmp_path, empty, 4096, None) _dd_write_aggregate( tmp_path, [ _dd_agg_row(4096, f"eval_results/{old}/results_2026-06-26T13-00-22.596040.json", 0.83), - _dd_agg_row(4096, f"eval_results/{new}/results_2026-06-27T04-28-31.838775.json", 0.95), + _dd_agg_row( + 4096, + f"eval_results/{new}/results_kimi_vendor_2026-06-27T04-28-31.838775.json", + 0.95, + ), _dd_agg_row(4096, f"eval_results/{mid}/results_2026-06-26T19-00-52.356121.json", 0.78), ], ) diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 0cfd1d2662..549547b203 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -313,6 +313,16 @@ def normalized_runner(value: Any) -> str: return str(value or "").lower() +LEGACY_EVAL_SUITE = "" + + +def eval_suite_identity(row: dict[str, Any]) -> Any: + """Return an explicit suite or the compatibility identity for old artifacts.""" + if "eval_suite" in row: + return row["eval_suite"] + return LEGACY_EVAL_SUITE + + def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: """Build an eval identity from one aggregate row.""" if as_bool(row.get("is_multinode", False)): @@ -322,6 +332,7 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: row.get("model_prefix", row.get("infmax_model_prefix")), row.get("framework"), row.get("precision"), + eval_suite_identity(row), row.get("spec_decoding", "none"), as_int(row.get("isl", 8192), 8192), as_int(row.get("osl", 1024), 1024), @@ -347,6 +358,7 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: row.get("model_prefix", row.get("infmax_model_prefix")), row.get("framework"), row.get("precision"), + eval_suite_identity(row), row.get("spec_decoding", "none"), as_int(row.get("isl", 8192), 8192), as_int(row.get("osl", 1024), 1024), From b7d0d7e35ac2c1290d25676e2a15c966199f3b02 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:36:45 -0500 Subject: [PATCH 02/99] refactor: simplify and expose tool-use eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:精简工具调用评估实现并接入工作流选择路径 --- .github/workflows/benchmark-tmpl.yml | 6 + .github/workflows/e2e-tests.yml | 11 + benchmarks/benchmark_lib.sh | 124 ++------ utils/collect_eval_results.py | 5 +- utils/evals/EVALS.md | 99 ++----- utils/evals/kimi_vendor_eval.py | 41 +-- utils/evals/test_kimi_vendor_eval.py | 241 ++++++---------- utils/evals/test_run_eval_dispatch.py | 264 +++--------------- utils/test_collect_eval_results.py | 11 +- .../test_validate_reusable_sweep_artifacts.py | 23 +- utils/validate_reusable_sweep_artifacts.py | 10 +- 11 files changed, 204 insertions(+), 631 deletions(-) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 4dc036a06d..e982384336 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -85,6 +85,11 @@ on: type: boolean required: false default: false + eval-framework: + description: "Eval runner (lm-eval, swebench, or tool-use)" + type: string + required: false + default: "lm-eval" random-range-ratio: required: false type: string @@ -173,6 +178,7 @@ env: DISAGG: ${{ inputs.disagg }} RUN_EVAL: ${{ inputs.run-eval }} EVAL_ONLY: ${{ inputs.eval-only }} + EVAL_FRAMEWORK: ${{ inputs.eval-framework }} # Agentic-coding env. Fixed-seq-len jobs leave these empty. SCENARIO_TYPE: ${{ inputs.scenario-type }} SCENARIO_SUBDIR: ${{ inputs.scenario-type == 'agentic-coding' && 'agentic/' || 'fixed_seq_len/' }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9648605fad..83b48333f9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -45,6 +45,11 @@ on: required: false type: string default: "" + eval-framework: + description: "Agentic eval runner (lm-eval, swebench, or tool-use)" + required: false + type: string + default: "lm-eval" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -125,6 +130,11 @@ on: required: false type: string default: "" + eval-framework: + description: "Agentic eval runner (lm-eval, swebench, or tool-use)" + required: false + type: string + default: "lm-eval" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -459,6 +469,7 @@ jobs: eval-only: true eval-limit: ${{ inputs.eval-limit }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} + eval-framework: ${{ inputs.eval-framework }} scenario-type: agentic-coding ref: ${{ inputs.ref }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 5519682c8f..b621c9d923 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -838,87 +838,23 @@ _install_tool_use_eval_deps() { "pytest==8.4.2" } -_kimi_vendor_checkout_is_valid() { - local checkout_dir="$1" - local expected_ref="$2" - local checkout_ref checkout_status tracked_status untracked_files ignored_files - - [ -f "${checkout_dir}/LICENSE" ] \ - && [ -f "${checkout_dir}/pyproject.toml" ] \ - && [ -f "${checkout_dir}/tests/conftest.py" ] \ - && [ -f "${checkout_dir}/tests/__init__.py" ] \ - && [ -f "${checkout_dir}/tests/tool_call_json_schema/conftest.py" ] \ - && [ -f "${checkout_dir}/tests/tool_call_json_schema/validator.py" ] \ - && [ -f "${checkout_dir}/tests/tool_call_json_schema/test_tool_call_json_schema.py" ] \ - && [ -d "${checkout_dir}/testdata/walle_validator_cases/validator_cases" ] \ - || return 1 - checkout_ref="$(git -C "$checkout_dir" rev-parse HEAD 2>/dev/null)" \ - || return 1 - [ "$checkout_ref" = "$expected_ref" ] || return 1 - checkout_status="$( - git -C "$checkout_dir" status --porcelain --untracked-files=all -- \ - LICENSE \ - pyproject.toml \ - tests/conftest.py \ - tests/__init__.py \ - tests/tool_call_json_schema \ - testdata/walle_validator_cases - )" || return 1 - [ -z "$checkout_status" ] || return 1 - tracked_status="$( - git -C "$checkout_dir" status --porcelain --untracked-files=no - )" || return 1 - [ -z "$tracked_status" ] || return 1 - untracked_files="$( - git -C "$checkout_dir" ls-files --others --exclude-standard -- \ - . ':(exclude,top,glob).pytest_cache/**' - )" || return 1 - [ -z "$untracked_files" ] || return 1 - ignored_files="$( - git -C "$checkout_dir" ls-files --others --ignored --exclude-standard -- \ - . ':(exclude,top,glob).pytest_cache/**' - )" || return 1 - [ -z "$ignored_files" ] -} - _prepare_kimi_vendor_verifier() { local repo_url="$1" local verifier_ref="$2" local checkout_dir - if [ -n "${KIMI_VENDOR_VERIFIER_DIR:-}" ]; then - checkout_dir="$KIMI_VENDOR_VERIFIER_DIR" - if ! _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then - echo "ERROR: KIMI_VENDOR_VERIFIER_DIR must be at ${verifier_ref}" >&2 - echo "ERROR: required verifier sources must be present and unmodified" >&2 - return 2 - fi - KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" - return 0 - fi - - checkout_dir="/tmp/kimi-vendor-verifier-${verifier_ref}" - if _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then - KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" - return 0 - fi - command -v git >/dev/null 2>&1 || { echo "ERROR: git is required to fetch Kimi-Vendor-Verifier" >&2 return 1 } - rm -rf "$checkout_dir" - mkdir -p "$(dirname "$checkout_dir")" || return $? + checkout_dir="$(mktemp -d /tmp/kimi-vendor-verifier-XXXXXX)" || return $? if ! ( git init -q "$checkout_dir" \ && git -C "$checkout_dir" remote add origin "$repo_url" \ - && git -C "$checkout_dir" config remote.origin.promisor true \ - && git -C "$checkout_dir" config remote.origin.partialclonefilter blob:none \ && git -C "$checkout_dir" fetch -q --filter=blob:none --depth=1 \ origin "$verifier_ref" \ && git -C "$checkout_dir" update-ref HEAD FETCH_HEAD \ && git -C "$checkout_dir" sparse-checkout set --no-cone \ - /LICENSE \ /pyproject.toml \ /tests/conftest.py \ /tests/__init__.py \ @@ -930,11 +866,6 @@ _prepare_kimi_vendor_verifier() { echo "ERROR: failed to fetch Kimi-Vendor-Verifier at ${verifier_ref}" >&2 return 1 fi - if ! _kimi_vendor_checkout_is_valid "$checkout_dir" "$verifier_ref"; then - rm -rf "$checkout_dir" - echo "ERROR: fetched Kimi-Vendor-Verifier checkout is incomplete" >&2 - return 1 - fi KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" } @@ -990,7 +921,6 @@ run_tool_use_eval() { return 2 ;; esac - export EVAL_FRAMEWORK=tool-use export EVAL_SUITE="$eval_suite" local _repo_root @@ -1001,46 +931,43 @@ run_tool_use_eval() { mkdir -p "$results_dir" || return $? export EVAL_RESULT_DIR="$results_dir" - local setup_rc integration_error - if _require_tool_use_python; then - : - else + local setup_rc=0 integration_error="" + _require_tool_use_python || { setup_rc=$? integration_error="tool-use Python version check failed with exit code ${setup_rc}" - echo "ERROR: ${integration_error}" >&2 - _write_tool_use_integration_error \ - "$adapter_path" "$model_name" "$results_dir" "$integration_error" - return "$setup_rc" - fi - if [ "${INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY:-false}" != "true" ]; then + } + if [ "$setup_rc" -eq 0 ] \ + && [ "${INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY:-false}" != "true" ]; then if _install_tool_use_eval_deps; then export INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY=true else setup_rc=$? integration_error="tool-use dependency installation failed with exit code ${setup_rc}" - echo "ERROR: ${integration_error}" >&2 - _write_tool_use_integration_error \ - "$adapter_path" "$model_name" "$results_dir" "$integration_error" - return "$setup_rc" fi fi - if _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref"; then - : - else - setup_rc=$? - integration_error="tool-use verifier checkout failed with exit code ${setup_rc}" + if [ "$setup_rc" -eq 0 ]; then + _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref" || { + setup_rc=$? + integration_error="tool-use verifier checkout failed with exit code ${setup_rc}" + } + fi + if [ "$setup_rc" -ne 0 ]; then echo "ERROR: ${integration_error}" >&2 _write_tool_use_integration_error \ "$adapter_path" "$model_name" "$results_dir" "$integration_error" return "$setup_rc" fi + local eval_rc=0 python3 "$adapter_path" \ --verifier-dir "$KIMI_VENDOR_VERIFIER_CHECKOUT_DIR" \ --base-url "http://127.0.0.1:${port}/v1" \ --api-key EMPTY \ --model "$model_name" \ - --output-dir "$results_dir" + --output-dir "$results_dir" \ + || eval_rc=$? + rm -rf "$KIMI_VENDOR_VERIFIER_CHECKOUT_DIR" || true + return "$eval_rc" } _eval_patches_dir() { @@ -1161,14 +1088,7 @@ run_lm_eval() { tasks_dir="$_repo_root/$tasks_dir" fi - local effective_suite="${EVAL_SUITE:-}" - local task_basename - if [ -z "$effective_suite" ]; then - task_basename="${tasks_dir##*/}" - effective_suite="${task_basename%.yaml}" - effective_suite="${effective_suite%.yml}" - fi - export EVAL_SUITE="$effective_suite" + export EVAL_TASKS_DIR="$tasks_dir" if [ "${INFERENCEX_LM_EVAL_RUNTIME_READY:-false}" != "true" ]; then _install_lm_eval_deps @@ -1377,7 +1297,6 @@ append_lm_eval_summary() { fi fi fi - local eval_framework="${EVAL_FRAMEWORK:-lm-eval}" local eval_suite="${EVAL_SUITE:-}" if [ -z "$eval_suite" ] && [ -n "${EVAL_TASKS_DIR:-}" ]; then eval_suite="$(basename "${EVAL_TASKS_DIR}")" @@ -1391,7 +1310,6 @@ append_lm_eval_summary() { "framework": "${fw:-unknown}", "precision": "${prec:-unknown}", "spec_decoding": "${SPEC_DECODING:-}", - "eval_framework": "${eval_framework}", "eval_suite": "${eval_suite}", "tp": ${TP:-1}, "pp": ${PP_SIZE:-1}, @@ -1809,10 +1727,6 @@ run_eval() { fi local framework="${EVAL_FRAMEWORK:-${cli_framework:-$scenario_default}}" - if [ "$framework" = "lm_eval" ]; then - framework="lm-eval" - fi - export EVAL_FRAMEWORK="$framework" # Tool-use uses the verifier's fixed request budget and does not consume # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for that path. diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 7bc49c5d8c..fd7c0b1d05 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -284,9 +284,8 @@ def build_row(meta: Dict[str, Any], m: Dict[str, Any]) -> Dict[str, Any]: 'source': m.get('source'), } - for metadata_field in ('eval_framework', 'eval_suite'): - if metadata_field in meta: - row[metadata_field] = meta[metadata_field] + if 'eval_suite' in meta: + row['eval_suite'] = meta['eval_suite'] # Add universal score field (primary metric for unified comparison) if m.get('strict') is not None: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 07aa48b4de..64de3ced31 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -40,84 +40,43 @@ malformed metadata, duplicates, or raw/aggregate mismatches are not. See ## How? `run_eval` in `benchmarks/benchmark_lib.sh` dispatches to the selected eval -framework against the server's OpenAI-compatible endpoint. The default is -[lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) -(`lm-eval`) with GSM8K. Existing fixed-sequence and agentic paths preserve that -default, and explicit agentic runs can still select SWE-bench. +runner. Existing jobs continue to use lm-eval with GSM8K by default. -The Phase 1 tool-use suite is opt-in. The Kimi-K3 B300 vLLM agentic launcher, -like every existing launcher, continues to select lm-eval/GSM8K by default. To -run the suite after its server is ready, use the existing entrypoint: +The Phase 1 tool-use smoke is opt-in and single-node only. Select it with the +`eval-framework: tool-use` input on `e2e-tests.yml`, or invoke it after a server +is ready: ```bash -EVAL_FRAMEWORK=tool-use EVAL_SUITE=kimi_tool_call_schema \ - run_eval --port "$PORT" +EVAL_FRAMEWORK=tool-use run_eval --port "$PORT" ``` -`run_tool_use_eval` supplies `kimi_tool_call_schema` when `EVAL_SUITE` is unset -for a manual `run_eval --framework tool-use` call and rejects every other suite. -The compatibility result continues through the existing collector, suite-aware -artifact identity, and strict `1.0` threshold. -Phase 1 is single-node only and rejects `IS_MULTINODE=true` or `1`; the -multi-node workflow does not yet preserve the stock native report. - ### Stock Kimi tool-call schema smoke -This suite runs the unmodified +The smoke runs the unmodified [MoonshotAI/Kimi-Vendor-Verifier](https://github.com/MoonshotAI/Kimi-Vendor-Verifier) -at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Its bundled Walle -schema corpus is sourced from MoonshotAI/walle commit -`cc1c6b7dab5496d5184677ecf4c3b95fc1bd1606` (`v0.1.10`). The upstream -prompt, schema loading and selection, request construction, non-stream and -stream assembly, argument validation, and report generation are all stock. -InferenceX owns only the subprocess invocation and compatibility projection. - -Python 3.12 or newer is required; the runner fails with a version error before -installing or checking out anything on older Python. At runtime it installs only -`httpx[http2]==0.28.1`, `openai==2.14.0`, `jsonschema==4.25.1`, and -`pytest==8.4.2`. It then makes a network checkout from GitHub using a sparse, -detached checkout of the pinned verifier commit containing only: - -- `LICENSE` and `pyproject.toml`; -- `tests/__init__.py`, `tests/conftest.py`, and - `tests/tool_call_json_schema/`; -- `testdata/walle_validator_cases/`. - -An explicitly supplied `KIMI_VENDOR_VERIFIER_DIR` is reused only when it is at -that exact commit, required sources are unmodified, and no extra checkout files -can override the verifier (root `.pytest_cache/` is ignored). The verifier -project and its unrelated benchmark dependencies are not installed. - -The thin `utils/evals/kimi_vendor_eval.py` wrapper runs upstream +at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run creates a fresh +sparse checkout containing the upstream pytest configuration, tool-call schema +tests, and bundled Walle cases. InferenceX does not install the verifier package +or reimplement its request, streaming, retry, or validation logic. + +Python 3.12 or newer is required. The runner installs the minimal pinned runtime +(`httpx[http2]`, `openai`, `jsonschema`, and `pytest`), then runs upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: -- base URL `http://127.0.0.1:${PORT}/v1`, API key `EMPTY`, and model - `${MODEL_NAME:-$MODEL}`; -- `--case-dir testdata/walle_validator_cases/validator_cases`, - `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; -- `--tool-json-report /kimi_vendor_report.json`. - -That stock selection chooses Walle case `TestAdditionalProperties:1` and -upstream parametrizes it in both `non-stream` and `stream` modes, for two -results. Requests use upstream's `openai.Client(timeout=120)` unchanged, so -OpenAI SDK 2.14.0's stock retry policy remains in effect, including its default -two retries for eligible connection, timeout, 408, 409, 429, and 5xx failures. -InferenceX does not add request retries or make the two modes concurrent. -`EVAL_CONCURRENT_REQUESTS` remains matrix metadata; multi-value batched -concurrency remains supported only by `lm-eval`. - -The unchanged native `kimi_vendor_report.json` is uploaded alongside the -collector-compatible `results_kimi_vendor_.json`. The -compatibility score is `passed / 2` for task `kimi_tool_call_schema`, primary metric -`exact_match,strict-match`, and effective sample count two. Success requires -pytest to exit zero and exactly two upstream mode results to pass. A setup or -collection failure still produces a zero-score compatibility result with -integration error metadata; the native report can be absent when upstream -cannot collect. - -Phase 1 intentionally covers one stock object-schema case only. It does not -measure broader schema coverage, tool selection among multiple tools, parallel -tool calls, multi-turn tool execution, or general agent quality. +- the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; +- `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; +- the bundled Walle case directory and `--tool-json-report`. + +The selection is `TestAdditionalProperties:1`, parametrized upstream in +non-streaming and streaming modes. The unchanged native report is uploaded as +`kimi_vendor_report.json`. `utils/evals/kimi_vendor_eval.py` only projects its +two outcomes into the existing eval result shape. Both must pass, so the +`kimi_tool_call_schema` threshold is `1.0`. Setup and collection failures emit a +zero-score result with error metadata. + +This smoke validates one object-schema tool call. It does not cover tool choice, +parallel calls, multi-turn execution, or general agent quality. Multi-value +batched concurrency and multi-node execution are unsupported. ### Benchmark script flow @@ -152,7 +111,7 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_install_tool_use_eval_deps` | Installs the minimal pinned stock-verifier runtime | -| `_prepare_kimi_vendor_verifier` | Prepares or validates the pinned sparse checkout | +| `_prepare_kimi_vendor_verifier` | Fetches a fresh pinned sparse checkout | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | @@ -220,7 +179,6 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `em_flexible` | Flexible extraction (looser number matching) | | `n_eff` | Number of samples evaluated | | `task` | Eval task name (e.g., `gsm8k`) | -| `eval_framework` | Eval runner identity (for example, `lm-eval` or `tool-use`) | | `eval_suite` | Explicit suite identity used for collection and artifact reuse | ### Environment variables @@ -233,7 +191,6 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Eval suite metadata; explicit values take precedence | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | -| `KIMI_VENDOR_VERIFIER_DIR` | generated pinned checkout | Optional pre-existing verifier checkout; exact ref and required paths are validated | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | | `EVAL_CONCURRENT_REQUESTS` | `64` | Concurrent requests during eval; a space-separated list enables sequential batched evals against one live engine | | `EVAL_LIMIT` | empty | Limit eval to first N instances (smoke tests); empty = full set | diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index c01343fe15..a71845cc41 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -14,7 +14,7 @@ TASK_NAME = "kimi_tool_call_schema" NATIVE_REPORT_FILENAME = "kimi_vendor_report.json" -COMPATIBILITY_GLOB = "results_kimi_vendor_*.json" +COMPATIBILITY_GLOB = "results_*.json" EXPECTED_MODES = {"non-stream", "stream"} @@ -23,7 +23,7 @@ def prepare_compatibility_path(output_dir: Path) -> Path: for stale_path in output_dir.glob(COMPATIBILITY_GLOB): stale_path.unlink() timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") - return output_dir / f"results_kimi_vendor_{timestamp}.json" + return output_dir / f"results_{timestamp}.json" def build_pytest_command( @@ -96,16 +96,15 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: if total != len(results) or passed != result_passes: raise ValueError("report summary does not match result records") + if ( + total != 2 + or len(results) != 2 + or set(modes) != EXPECTED_MODES + or len(modes) != len(set(modes)) + ): + raise ValueError("report does not contain the expected stream modes") score = passed / 2.0 - compatibility = _compatibility_result(model, score) - complete_pass = ( - total == 2 - and passed == 2 - and len(results) == 2 - and set(modes) == EXPECTED_MODES - and len(modes) == len(set(modes)) - ) - return compatibility, complete_pass + return _compatibility_result(model, score), passed == 2 def _compatibility_result( @@ -114,7 +113,6 @@ def _compatibility_result( result: dict[str, Any] = { "lm_eval_version": "kimi-vendor-verifier", "model_name": model, - "model_args": f"pretrained={model}", "results": { TASK_NAME: { "exact_match,strict-match": score, @@ -123,25 +121,10 @@ def _compatibility_result( }, "configs": { TASK_NAME: { - "task": TASK_NAME, - "output_type": "generate_until", - "num_fewshot": 0, - "repeats": 1, - "metric_list": [ - { - "metric": "exact_match", - "aggregation": "mean", - "higher_is_better": True, - } - ], - "filter_list": [ - {"name": "strict-match", "filter": [{"function": "identity"}]} - ], + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], } }, - "versions": {TASK_NAME: 1}, - "n-shot": {TASK_NAME: 0}, - "higher_is_better": {TASK_NAME: {"exact_match": True}}, "n-samples": {TASK_NAME: {"original": 2, "effective": 2}}, } if integration_error is not None: diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index bfeb49819d..ff223d3a35 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -4,6 +4,7 @@ import sys from pathlib import Path from types import SimpleNamespace +from typing import Any import pytest @@ -12,21 +13,11 @@ import kimi_vendor_eval as kve -def _native_report(*, stream_status: str = "passed") -> dict: +def _report(stream_status: str = "passed") -> dict[str, Any]: statuses = ["passed", stream_status] - by_status: dict[str, int] = {} - for status in statuses: - by_status[status] = by_status.get(status, 0) + 1 + by_status = {status: statuses.count(status) for status in set(statuses)} return { - "summary": { - "total": 2, - "by_status": by_status, - "by_selection_reason": {"object_schema": 2}, - "by_mode": { - "non-stream": {"passed": 1}, - "stream": {stream_status: 1}, - }, - }, + "summary": {"total": 2, "by_status": by_status}, "results": [ {"mode": "non-stream", "status": "passed"}, {"mode": "stream", "status": stream_status}, @@ -34,37 +25,31 @@ def _native_report(*, stream_status: str = "passed") -> dict: } -def _compatibility_file(output_dir: Path) -> Path: - matches = list(output_dir.glob(kve.COMPATIBILITY_GLOB)) - assert len(matches) == 1 +def _result(output_dir: Path) -> dict[str, Any]: + paths = list(output_dir.glob(kve.COMPATIBILITY_GLOB)) + assert len(paths) == 1 assert re.fullmatch( - r"results_kimi_vendor_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", - matches[0].name, + r"results_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", + paths[0].name, ) - return matches[0] - - -def _projected(output_dir: Path) -> dict: - return json.loads(_compatibility_file(output_dir).read_text(encoding="utf-8")) + return json.loads(paths[0].read_text()) def _score(output_dir: Path) -> float: - return _projected(output_dir)["results"][kve.TASK_NAME][ + return _result(output_dir)["results"][kve.TASK_NAME][ "exact_match,strict-match" ] -def test_builds_exact_upstream_pytest_command(tmp_path: Path) -> None: +def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: report = tmp_path / kve.NATIVE_REPORT_FILENAME - command = kve.build_pytest_command( + assert kve.build_pytest_command( base_url="http://127.0.0.1:8000/v1", api_key="EMPTY", model="test-model", report_path=report, - ) - - assert command == [ + ) == [ sys.executable, "-m", "pytest", @@ -90,198 +75,128 @@ def test_builds_exact_upstream_pytest_command(tmp_path: Path) -> None: ] -def test_full_pass_projects_score_and_preserves_native_report( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch +@pytest.mark.parametrize( + ("stream_status", "return_code", "expected_pass", "expected_score"), + (("passed", 0, True, 1.0), ("failed", 1, False, 0.5)), +) +def test_projects_upstream_outcomes( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + stream_status: str, + return_code: int, + expected_pass: bool, + expected_score: float, ) -> None: - verifier_dir = tmp_path / "verifier" - verifier_dir.mkdir() output_dir = tmp_path / "output" - output_dir.mkdir() - (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( - "stale", encoding="utf-8" - ) - native_bytes = (json.dumps(_native_report(), indent=2) + "\n").encode() - invocation: dict[str, object] = {} + native_bytes = json.dumps(_report(stream_status)).encode() + invocation: dict[str, Any] = {} def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: invocation.update(command=command, cwd=cwd, check=check) - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / kve.NATIVE_REPORT_FILENAME).write_bytes(native_bytes) - return SimpleNamespace(returncode=0) + Path(command[command.index("--tool-json-report") + 1]).write_bytes( + native_bytes + ) + return SimpleNamespace(returncode=return_code) monkeypatch.setattr(kve.subprocess, "run", fake_run) - passed = kve.run_evaluation( - verifier_dir=verifier_dir, - base_url="http://localhost:8000/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, + assert ( + kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + is expected_pass ) - - assert passed - assert invocation["cwd"] == verifier_dir + assert invocation["cwd"] == tmp_path assert invocation["check"] is False - assert invocation["command"] == kve.build_pytest_command( - base_url="http://localhost:8000/v1", - api_key="EMPTY", - model="model-a", - report_path=(output_dir / kve.NATIVE_REPORT_FILENAME).resolve(), - ) + assert _score(output_dir) == expected_score assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes - projected = _projected(output_dir) - assert _score(output_dir) == 1.0 - assert projected["n-samples"][kve.TASK_NAME] == {"original": 2, "effective": 2} - assert "integration_error" not in projected - - -def test_one_mode_failure_projects_partial_score_and_fails( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - output_dir = tmp_path / "output" - - def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: - report_path = Path(command[command.index("--tool-json-report") + 1]) - report_path.write_text(json.dumps(_native_report(stream_status="failed"))) - return SimpleNamespace(returncode=1) - - monkeypatch.setattr(kve.subprocess, "run", fake_run) - - passed = kve.run_evaluation( - verifier_dir=tmp_path, - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, - ) - - assert not passed - assert _score(output_dir) == 0.5 -@pytest.mark.parametrize("native_contents", [None, "{not-json"]) -def test_missing_or_malformed_report_writes_zero_score_with_error( +@pytest.mark.parametrize( + ("failure", "error_type"), + ( + (None, "FileNotFoundError"), + ("{bad-json", "JSONDecodeError"), + (OSError("boom"), "OSError"), + ), +) +def test_collection_failures_write_zero_score( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, - native_contents: str | None, + failure: str | OSError | None, + error_type: str, ) -> None: - output_dir = tmp_path / "output" - def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: - if native_contents is not None: - report_path = Path(command[command.index("--tool-json-report") + 1]) - report_path.write_text(native_contents, encoding="utf-8") + if isinstance(failure, OSError): + raise failure + if failure is not None: + Path(command[command.index("--tool-json-report") + 1]).write_text( + failure + ) return SimpleNamespace(returncode=1) monkeypatch.setattr(kve.subprocess, "run", fake_run) + output_dir = tmp_path / "output" - passed = kve.run_evaluation( + assert not kve.run_evaluation( verifier_dir=tmp_path, base_url="http://localhost/v1", api_key="EMPTY", model="model-a", output_dir=output_dir, ) - - projected = _projected(output_dir) - assert not passed + projected = _result(output_dir) assert _score(output_dir) == 0.0 - assert projected["integration_error"]["type"] in { - "FileNotFoundError", - "JSONDecodeError", - } - assert projected["integration_error"]["message"] + assert projected["integration_error"]["type"] == error_type -def test_collection_failure_cannot_project_a_stale_passing_report( +def test_failure_cannot_reuse_stale_outputs( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: output_dir = tmp_path / "output" output_dir.mkdir() native_report = output_dir / kve.NATIVE_REPORT_FILENAME - native_report.write_text(json.dumps(_native_report()), encoding="utf-8") + native_report.write_text(json.dumps(_report())) + (output_dir / "results_2000-01-01T00-00-00.000000.json").write_text("{}") - def collection_failure( - command: list[str], *, cwd: Path, check: bool - ) -> SimpleNamespace: + def fail_collection(*args: Any, **kwargs: Any) -> SimpleNamespace: assert not native_report.exists() return SimpleNamespace(returncode=2) - monkeypatch.setattr(kve.subprocess, "run", collection_failure) - - passed = kve.run_evaluation( - verifier_dir=tmp_path, - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, - ) - - projected = _projected(output_dir) - assert not passed - assert _score(output_dir) == 0.0 - assert projected["integration_error"]["type"] == "FileNotFoundError" - - -def test_subprocess_launch_failure_writes_zero_score_with_error( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - def fail_to_launch(*args: object, **kwargs: object) -> subprocess.CompletedProcess: - raise OSError("pytest could not launch") - - monkeypatch.setattr(kve.subprocess, "run", fail_to_launch) - output_dir = tmp_path / "output" + monkeypatch.setattr(kve.subprocess, "run", fail_collection) - passed = kve.run_evaluation( + assert not kve.run_evaluation( verifier_dir=tmp_path, base_url="http://localhost/v1", api_key="EMPTY", model="model-a", output_dir=output_dir, ) - - projected = _projected(output_dir) - assert not passed assert _score(output_dir) == 0.0 - assert projected["integration_error"] == { - "type": "OSError", - "message": "pytest could not launch", - } + assert not native_report.exists() - -def test_cli_integration_error_writes_failure_without_running_pytest( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - def unexpected_run(*args: object, **kwargs: object) -> None: - pytest.fail("integration-error mode must not launch pytest") - - monkeypatch.setattr(kve.subprocess, "run", unexpected_run) +def test_cli_setup_failure_clears_stale_outputs(tmp_path: Path) -> None: output_dir = tmp_path / "output" output_dir.mkdir() - (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( - "stale", encoding="utf-8" - ) - (output_dir / kve.NATIVE_REPORT_FILENAME).write_text( - json.dumps(_native_report()), encoding="utf-8" - ) + (output_dir / kve.NATIVE_REPORT_FILENAME).write_text(json.dumps(_report())) + (output_dir / "results_2000-01-01T00-00-00.000000.json").write_text("{}") - return_code = kve.main( + assert kve.main( [ "--model", "model-a", "--output-dir", str(output_dir), "--integration-error", - "pinned verifier checkout failed", + "checkout failed", ] - ) - - projected = _projected(output_dir) - assert return_code == 1 + ) == 1 + projected = _result(output_dir) assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() assert _score(output_dir) == 0.0 - assert projected["integration_error"] == { - "type": "RuntimeError", - "message": "pinned verifier checkout failed", - } \ No newline at end of file + assert projected["integration_error"]["message"] == "checkout failed" \ No newline at end of file diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 067f2165e0..1f6478934a 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -7,7 +7,6 @@ import subprocess from pathlib import Path -import pytest BENCHMARK_LIB = Path(__file__).resolve().parents[2] / "benchmarks" / "benchmark_lib.sh" @@ -16,9 +15,9 @@ run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } run_tool_use_eval() { echo "DISPATCH=tool-use"; } -append_lm_eval_summary() { echo "STAGED=summary FRAMEWORK=$EVAL_FRAMEWORK"; } +append_lm_eval_summary() { echo "STAGED=summary"; } export EVAL_MAX_MODEL_LEN=16384 -export EVAL_CONCURRENT_REQUESTS="${REQUESTED_CONC:-}" +export EVAL_CONCURRENT_REQUESTS="" run_eval ${CLI_FW:+--framework "$CLI_FW"} --port 8888 ''' @@ -29,7 +28,6 @@ def _dispatch( eval_only: str = "false", cli_fw=None, env_fw=None, - requested_conc=None, ) -> str: env = { **os.environ, @@ -40,14 +38,11 @@ def _dispatch( } env.pop("EVAL_FRAMEWORK", None) env.pop("CLI_FW", None) - env.pop("REQUESTED_CONC", None) env.pop("KV_OFFLOAD_BACKEND", None) if cli_fw is not None: env["CLI_FW"] = cli_fw if env_fw is not None: env["EVAL_FRAMEWORK"] = env_fw - if requested_conc is not None: - env["REQUESTED_CONC"] = str(requested_conc) res = subprocess.run( ["bash", "-c", _SCRIPT], env=env, text=True, capture_output=True, check=True ) @@ -66,7 +61,6 @@ def test_agentic_eval_only_stages_summary(): output = _dispatch(is_agentic="1", eval_only="true") assert "DISPATCH=lm-eval" in output assert "STAGED=summary" in output - assert "FRAMEWORK=lm-eval" in output def test_fixed_seqlen_eval_only_leaves_staging_to_recipe(): @@ -86,24 +80,14 @@ def test_env_can_force_swebench_on_fixed_seqlen(): assert "DISPATCH=swebench" in _dispatch(is_agentic="0", env_fw="swebench") -def test_cli_swebench_framework_is_canonical_in_metadata() -> None: - output = _dispatch( - is_agentic="1", - eval_only="true", - cli_fw="swebench", - ) - assert "DISPATCH=swebench" in output - assert "FRAMEWORK=swebench" in output def test_env_can_force_tool_use_on_agentic_eval() -> None: - output = _dispatch( + assert "DISPATCH=tool-use" in _dispatch( is_agentic="1", eval_only="true", env_fw="tool-use", ) - assert "DISPATCH=tool-use" in output - assert "FRAMEWORK=tool-use" in output def test_tool_use_skips_unused_model_context_loading() -> None: @@ -131,26 +115,12 @@ def test_tool_use_skips_unused_model_context_loading() -> None: assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout -def test_tool_use_accepts_single_matrix_concurrency_identity() -> None: - assert "DISPATCH=tool-use" in _dispatch( - is_agentic="1", - env_fw="tool-use", - requested_conc=64, - ) def test_recipe_lm_eval_arg_still_lm_eval_on_fixed_seqlen(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="0", cli_fw="lm-eval") -def test_lm_eval_alias_is_canonicalized_in_metadata() -> None: - output = _dispatch( - is_agentic="1", - eval_only="true", - cli_fw="lm_eval", - ) - assert "DISPATCH=lm-eval" in output - assert "FRAMEWORK=lm-eval" in output def _run_invalid_call(call: str) -> subprocess.CompletedProcess: @@ -201,36 +171,14 @@ def test_tool_use_rejects_multinode() -> None: assert "supports single-node evals only" in result.stderr -@pytest.mark.parametrize( - ("failure_stage", "failure_rc", "message"), - ( - ("python", 11, "tool-use Python version check failed"), - ("dependencies", 12, "tool-use dependency installation failed"), - ("checkout", 13, "tool-use verifier checkout failed"), - ), -) def test_tool_use_setup_failure_writes_compatibility_result( tmp_path: Path, - failure_stage: str, - failure_rc: int, - message: str, ) -> None: results_dir = tmp_path / "results" script = r''' source "$BENCHMARK_LIB" -_require_tool_use_python() { - [ "$FAILURE_STAGE" = python ] && return "$FAILURE_RC" - return 0 -} -_install_tool_use_eval_deps() { - [ "$FAILURE_STAGE" = dependencies ] && return "$FAILURE_RC" - return 0 -} -_prepare_kimi_vendor_verifier() { - [ "$FAILURE_STAGE" = checkout ] && return "$FAILURE_RC" - KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$FAKE_VERIFIER_DIR" - return 0 -} +_require_tool_use_python() { :; } +_install_tool_use_eval_deps() { return 12; } run_tool_use_eval --results-dir "$RESULTS_DIR" printf 'SETUP_RC=%s\n' "$?" ''' @@ -238,20 +186,14 @@ def test_tool_use_setup_failure_writes_compatibility_result( **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "RESULTS_DIR": str(results_dir), - "FAKE_VERIFIER_DIR": str(tmp_path / "verifier"), - "FAILURE_STAGE": failure_stage, - "FAILURE_RC": str(failure_rc), "MODEL": "test-model", "IS_MULTINODE": "false", "KV_OFFLOADING": "none", } for key in ( - "EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_RESULT_DIR", "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", - "KIMI_VENDOR_VERIFIER_DIR", - "KIMI_VENDOR_VERIFIER_CHECKOUT_DIR", "MODEL_NAME", ): env.pop(key, None) @@ -263,11 +205,11 @@ def test_tool_use_setup_failure_writes_compatibility_result( capture_output=True, check=True, ) - expected_message = f"{message} with exit code {failure_rc}" - - assert f"SETUP_RC={failure_rc}" in result.stdout - assert expected_message in result.stderr + message = "tool-use dependency installation failed with exit code 12" score_files = list(results_dir.glob("results*.json")) + + assert "SETUP_RC=12" in result.stdout + assert message in result.stderr assert len(score_files) == 1 score_result = json.loads(score_files[0].read_text()) assert ( @@ -276,128 +218,25 @@ def test_tool_use_setup_failure_writes_compatibility_result( ] == 0.0 ) - assert score_result["integration_error"]["message"] == expected_message + assert score_result["integration_error"]["message"] == message assert not (results_dir / "kimi_vendor_report.json").exists() -def test_kimi_vendor_checkout_rejects_source_changes(tmp_path: Path) -> None: - checkout = tmp_path / "verifier" - required_files = ( - "LICENSE", - "pyproject.toml", - "tests/conftest.py", - "tests/__init__.py", - "tests/tool_call_json_schema/conftest.py", - "tests/tool_call_json_schema/validator.py", - "tests/tool_call_json_schema/test_tool_call_json_schema.py", - "testdata/walle_validator_cases/validator_cases/case.jsonl", - ) - for relative_path in required_files: - path = checkout / relative_path - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(f"{relative_path}\n") - - subprocess.run(["git", "init", "-q", str(checkout)], check=True) - subprocess.run(["git", "-C", str(checkout), "add", "."], check=True) - subprocess.run( - [ - "git", - "-C", - str(checkout), - "-c", - "user.name=InferenceX Tests", - "-c", - "user.email=tests@inferencex.invalid", - "commit", - "-qm", - "fixture", - ], - check=True, - ) - verifier_ref = subprocess.run( - ["git", "-C", str(checkout), "rev-parse", "HEAD"], - check=True, - capture_output=True, - text=True, - ).stdout.strip() - - def checkout_is_valid() -> bool: - result = subprocess.run( - [ - "bash", - "-c", - 'source "$BENCHMARK_LIB"; ' - '_kimi_vendor_checkout_is_valid "$CHECKOUT" "$VERIFIER_REF"', - ], - env={ - **os.environ, - "BENCHMARK_LIB": str(BENCHMARK_LIB), - "CHECKOUT": str(checkout), - "VERIFIER_REF": verifier_ref, - "KV_OFFLOADING": "none", - }, - ) - return result.returncode == 0 - - assert checkout_is_valid() - pytest_cache = checkout / ".pytest_cache" / "v" / "cache" / "nodeids" - pytest_cache.parent.mkdir(parents=True) - pytest_cache.write_text("[]\n") - assert checkout_is_valid() - - with (checkout / ".git/info/exclude").open("a") as exclude_file: - exclude_file.write("\n/conftest.py\n") - root_override = checkout / "conftest.py" - root_override.write_text("# ignored root override\n") - assert not checkout_is_valid() - root_override.unlink() - - root_override.write_text("# staged root override\n") - subprocess.run( - ["git", "-C", str(checkout), "add", "-f", "conftest.py"], - check=True, - ) - assert not checkout_is_valid() - subprocess.run( - ["git", "-C", str(checkout), "reset", "-q", "HEAD", "--", "conftest.py"], - check=True, - ) - root_override.unlink() - - override = checkout / "tests/tool_call_json_schema/local_override.py" - override.write_text("# untracked override\n") - assert not checkout_is_valid() - override.unlink() - - validator = checkout / "tests/tool_call_json_schema/validator.py" - validator.write_text("# modified verifier\n") - assert not checkout_is_valid() def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: - shim_dir = tmp_path / "bin" - shim_dir.mkdir() - python_shim = shim_dir / "python3" - python_shim.write_text( - "#!/usr/bin/env bash\n" - "printf 'PYTHON_ARG=<%s>\\n' \"$@\"\n" - ) - python_shim.chmod( - python_shim.stat().st_mode | stat.S_IXUSR - ) results_dir = tmp_path / "results" verifier_dir = tmp_path / "verifier" script = r''' source "$BENCHMARK_LIB" -_require_tool_use_python() { echo "PYTHON_VERSION=OK"; } +_require_tool_use_python() { :; } _install_tool_use_eval_deps() { echo "INSTALL=UPSTREAM_MINIMAL"; } _prepare_kimi_vendor_verifier() { - printf 'CHECKOUT_REPO=%s\n' "$1" - printf 'CHECKOUT_REF=%s\n' "$2" + printf 'CHECKOUT=%s@%s\n' "$1" "$2" KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$VERIFIER_DIR" } +python3() { printf 'PYTHON_ARG=<%s>\n' "$@"; } run_tool_use_eval --port 9999 --results-dir "$RESULTS_DIR" -printf 'EVAL_FRAMEWORK=%s\n' "$EVAL_FRAMEWORK" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" printf 'RUNTIME_READY=%s\n' "$INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY" @@ -409,20 +248,18 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: "VERIFIER_DIR": str(verifier_dir), "MODEL": "test-model", "OPENAI_API_KEY": "must-not-be-forwarded", - "PATH": f"{shim_dir}:{os.environ['PATH']}", "KV_OFFLOADING": "none", "IS_MULTINODE": "false", } for key in ( - "EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_RESULT_DIR", "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", - "KIMI_VENDOR_VERIFIER_DIR", "KIMI_VENDOR_VERIFIER_CHECKOUT_DIR", "MODEL_NAME", ): env.pop(key, None) + result = subprocess.run( ["bash", "-c", script], env=env, @@ -431,35 +268,23 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: check=True, ) output = result.stdout - expected_adapter = ( - BENCHMARK_LIB.parents[1] / "utils/evals/kimi_vendor_eval.py" - ) + adapter = BENCHMARK_LIB.parents[1] / "utils/evals/kimi_vendor_eval.py" - assert "PYTHON_VERSION=OK" in output assert output.count("INSTALL=UPSTREAM_MINIMAL") == 1 assert ( - "CHECKOUT_REPO=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" - in output - ) - assert ( - "CHECKOUT_REF=b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" - in output - ) - assert f"PYTHON_ARG=<{expected_adapter}>" in output - for flag in ( - "--verifier-dir", - "--base-url", - "--api-key", - "--model", - "--output-dir", + "CHECKOUT=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" + "@b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + ) in output + for value in ( + adapter, + verifier_dir, + "http://127.0.0.1:9999/v1", + "EMPTY", + "test-model", + results_dir, ): - assert f"PYTHON_ARG=<{flag}>" in output - assert f"PYTHON_ARG=<{verifier_dir}>" in output - assert "PYTHON_ARG=" in output - assert "PYTHON_ARG=" in output - assert "PYTHON_ARG=" in output - assert f"PYTHON_ARG=<{results_dir}>" in output - assert "EVAL_FRAMEWORK=tool-use" in output + assert f"PYTHON_ARG=<{value}>" in output + assert "must-not-be-forwarded" not in output assert "EVAL_SUITE=kimi_tool_call_schema" in output assert f"EVAL_RESULT_DIR={results_dir}" in output assert "RUNTIME_READY=true" in output @@ -566,7 +391,7 @@ def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: "CONC": "7", "KV_OFFLOADING": "none", } - for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_TASKS_DIR"): + for key in ("EVAL_SUITE", "EVAL_TASKS_DIR"): env.pop(key, None) env.update(overrides) subprocess.run(["bash", "-c", script], env=env, check=True) @@ -576,44 +401,37 @@ def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: def test_summary_metadata_preserves_lm_eval_gsm8k_defaults(tmp_path: Path) -> None: meta = _summary_metadata(tmp_path) - assert meta["eval_framework"] == "lm-eval" assert meta["eval_suite"] == "gsm8k" assert meta["conc"] == 7 -def test_run_lm_eval_exports_cli_task_suite_to_metadata(tmp_path: Path) -> None: - work_dir = tmp_path / "work" - results_dir = tmp_path / "results" - work_dir.mkdir() - results_dir.mkdir() +def test_run_lm_eval_exports_cli_task_path(tmp_path: Path) -> None: script = r''' -set -e source "$BENCHMARK_LIB" -cd "$WORK_DIR" python3() { :; } export EVAL_MAX_MODEL_LEN=16384 export INFERENCEX_LM_EVAL_RUNTIME_READY=true run_lm_eval --task custom.yaml --results-dir "$RESULTS_DIR" -append_lm_eval_summary >/dev/null +printf 'EVAL_TASKS_DIR=%s\n' "$EVAL_TASKS_DIR" ''' env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), - "WORK_DIR": str(work_dir), - "RESULTS_DIR": str(results_dir), - "MODEL": "test-model", + "RESULTS_DIR": str(tmp_path / "results"), "MODEL_NAME": "test-model", "OPENAI_API_KEY": "EMPTY", "KV_OFFLOADING": "none", } - for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_TASKS_DIR"): - env.pop(key, None) - - subprocess.run(["bash", "-c", script], env=env, check=True) - meta = json.loads((work_dir / "meta_env.json").read_text()) + env.pop("EVAL_TASKS_DIR", None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) - assert meta["eval_framework"] == "lm-eval" - assert meta["eval_suite"] == "custom" + assert "EVAL_TASKS_DIR=custom.yaml" in result.stdout def test_summary_metadata_prefers_explicit_suite_then_task_basename( @@ -625,13 +443,11 @@ def test_summary_metadata_prefers_explicit_suite_then_task_basename( ) explicit = _summary_metadata( tmp_path / "explicit", - EVAL_FRAMEWORK="tool-use", EVAL_SUITE="kimi_tool_call_schema", EVAL_TASKS_DIR="/tmp/ignored.yaml", ) assert from_task["eval_suite"] == "custom_reasoning" - assert explicit["eval_framework"] == "tool-use" assert explicit["eval_suite"] == "kimi_tool_call_schema" diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 200be70e64..ba9ebef44c 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -21,20 +21,15 @@ def test_build_row_preserves_sequence_lengths() -> None: assert row["isl"] == 1024 assert row["osl"] == 1024 - assert "eval_framework" not in row assert "eval_suite" not in row -def test_build_row_preserves_explicit_eval_metadata() -> None: +def test_build_row_preserves_explicit_eval_suite() -> None: row = build_row( - { - "eval_framework": "tool-use", - "eval_suite": "kimi_tool_call_schema", - }, + {"eval_suite": "kimi_tool_call_schema"}, {"task": "kimi_tool_call_schema"}, ) - assert row["eval_framework"] == "tool-use" assert row["eval_suite"] == "kimi_tool_call_schema" @@ -82,7 +77,6 @@ def test_collect_eval_rows_expands_batched_concurrencies( "completed_eval_concs": [4, 16], "failed_eval_concs": [], "conc": 4, - "eval_framework": "lm-eval", "eval_suite": "gsm8k", })) _write_lm_eval_result( @@ -98,7 +92,6 @@ def test_collect_eval_rows_expands_batched_concurrencies( assert [row["conc"] for row in rows] == [4, 16] assert [row["score"] for row in rows] == [0.90, 0.91] - assert {row["eval_framework"] for row in rows} == {"lm-eval"} assert {row["eval_suite"] for row in rows} == {"gsm8k"} diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index 285bb6fcb3..cfa0a1df1a 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -673,23 +673,18 @@ def _dd_write_aggregate(root: Path, rows: list[dict]) -> Path: def _dd_write_legacy_raw( - root: Path, - name: str, - conc: int, - timestamp: str | None, - result_prefix: str = "results_", + root: Path, name: str, conc: int, timestamp: str | None ) -> None: artifact_dir = root / name artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text(json.dumps(_dd_meta(conc))) if timestamp is not None: - (artifact_dir / f"{result_prefix}{timestamp}.json").write_text("{}") + (artifact_dir / f"results_{timestamp}.json").write_text("{}") def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: # Three reruns of one eval plus a result-less attempt, mirroring a flaky # config retried until it passed. - # The latest rerun uses the tool-use adapter's timestamped result prefix. old, mid, new, empty = ( "eval_minimaxm3_conc4096_b300-nv_15", "eval_minimaxm3_conc4096_b300-nv_16", @@ -698,23 +693,13 @@ def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: ) _dd_write_legacy_raw(tmp_path, old, 4096, "2026-06-26T13-00-22.596040") _dd_write_legacy_raw(tmp_path, mid, 4096, "2026-06-26T19-00-52.356121") - _dd_write_legacy_raw( - tmp_path, - new, - 4096, - "2026-06-27T04-28-31.838775", - result_prefix="results_kimi_vendor_", - ) + _dd_write_legacy_raw(tmp_path, new, 4096, "2026-06-27T04-28-31.838775") _dd_write_legacy_raw(tmp_path, empty, 4096, None) _dd_write_aggregate( tmp_path, [ _dd_agg_row(4096, f"eval_results/{old}/results_2026-06-26T13-00-22.596040.json", 0.83), - _dd_agg_row( - 4096, - f"eval_results/{new}/results_kimi_vendor_2026-06-27T04-28-31.838775.json", - 0.95, - ), + _dd_agg_row(4096, f"eval_results/{new}/results_2026-06-27T04-28-31.838775.json", 0.95), _dd_agg_row(4096, f"eval_results/{mid}/results_2026-06-26T19-00-52.356121.json", 0.78), ], ) diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 549547b203..8942d88fbf 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -316,12 +316,6 @@ def normalized_runner(value: Any) -> str: LEGACY_EVAL_SUITE = "" -def eval_suite_identity(row: dict[str, Any]) -> Any: - """Return an explicit suite or the compatibility identity for old artifacts.""" - if "eval_suite" in row: - return row["eval_suite"] - return LEGACY_EVAL_SUITE - def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: """Build an eval identity from one aggregate row.""" @@ -332,7 +326,7 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: row.get("model_prefix", row.get("infmax_model_prefix")), row.get("framework"), row.get("precision"), - eval_suite_identity(row), + row.get("eval_suite", LEGACY_EVAL_SUITE), row.get("spec_decoding", "none"), as_int(row.get("isl", 8192), 8192), as_int(row.get("osl", 1024), 1024), @@ -358,7 +352,7 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: row.get("model_prefix", row.get("infmax_model_prefix")), row.get("framework"), row.get("precision"), - eval_suite_identity(row), + row.get("eval_suite", LEGACY_EVAL_SUITE), row.get("spec_decoding", "none"), as_int(row.get("isl", 8192), 8192), as_int(row.get("osl", 1024), 1024), From da29994e2faa6a720969d05cf9540679f08bb144 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:34:56 -0500 Subject: [PATCH 03/99] refactor: isolate and clarify verifier integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:隔离 Kimi 验证器依赖并明确供应商适配边界,同时加入执行超时和通用结果格式标识。 --- .github/workflows/benchmark-tmpl.yml | 12 ++- .github/workflows/e2e-tests.yml | 15 +++- benchmarks/benchmark_lib.sh | 123 ++++++++++++++++--------- utils/collect_eval_results.py | 9 +- utils/evals/EVALS.md | 40 ++++++--- utils/evals/kimi_vendor_eval.py | 31 ++++++- utils/evals/test_kimi_vendor_eval.py | 81 ++++++++++------- utils/evals/test_run_eval_dispatch.py | 124 ++++++++++++++++++-------- utils/test_collect_eval_results.py | 23 ++++- 9 files changed, 324 insertions(+), 134 deletions(-) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index e982384336..afafe2585f 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -86,10 +86,15 @@ on: required: false default: false eval-framework: - description: "Eval runner (lm-eval, swebench, or tool-use)" + description: "Eval runner (lm-eval, swebench, or kimi-vendor)" type: string required: false default: "lm-eval" + eval-suite: + description: "Suite interpreted by the selected eval runner" + type: string + required: false + default: "" random-range-ratio: required: false type: string @@ -179,6 +184,7 @@ env: RUN_EVAL: ${{ inputs.run-eval }} EVAL_ONLY: ${{ inputs.eval-only }} EVAL_FRAMEWORK: ${{ inputs.eval-framework }} + EVAL_SUITE: ${{ inputs.eval-suite }} # Agentic-coding env. Fixed-seq-len jobs leave these empty. SCENARIO_TYPE: ${{ inputs.scenario-type }} SCENARIO_SUBDIR: ${{ inputs.scenario-type == 'agentic-coding' && 'agentic/' || 'fixed_seq_len/' }} @@ -398,7 +404,7 @@ jobs: path: | meta_env.json results*.json - kimi_vendor_report.json + *_vendor_report.json sample*.jsonl agent_preds.json predictions.jsonl @@ -416,7 +422,7 @@ jobs: rm -f meta_env.json || true # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true - rm -f kimi_vendor_report.json || true + rm -f -- ./*_vendor_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 83b48333f9..6b9c67410b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,10 +46,15 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or tool-use)" + description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" + eval-suite: + description: "Agentic eval suite interpreted by the selected runner" + required: false + type: string + default: "" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -131,10 +136,15 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or tool-use)" + description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" + eval-suite: + description: "Agentic eval suite interpreted by the selected runner" + required: false + type: string + default: "" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -470,6 +480,7 @@ jobs: eval-limit: ${{ inputs.eval-limit }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite }} scenario-type: agentic-coding ref: ${{ inputs.ref }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index b621c9d923..1c1c3d8f5f 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -819,25 +819,37 @@ _install_lm_eval_deps() { fi } -_require_tool_use_python() { +_require_kimi_vendor_python() { if python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))'; then return 0 fi local python_version python_version="$(python3 -c 'import platform; print(platform.python_version())' 2>/dev/null || printf 'unavailable')" - echo "ERROR: tool-use requires Python >=3.12 (python3 is ${python_version})" >&2 + echo "ERROR: Kimi Vendor Verifier requires Python >=3.12 (python3 is ${python_version})" >&2 return 2 } -_install_tool_use_eval_deps() { - python3 -m pip install -q --no-cache-dir --break-system-packages \ +_install_kimi_vendor_eval_deps() { + local target_dir="$1" + python3 -m pip install -q --no-cache-dir --target "$target_dir" \ "httpx[http2]==0.28.1" \ "openai==2.14.0" \ "jsonschema==4.25.1" \ "pytest==8.4.2" } +_prepare_kimi_vendor_runtime() { + local runtime_dir install_rc=0 + runtime_dir="$(mktemp -d /tmp/kimi-vendor-runtime-XXXXXX)" || return $? + _install_kimi_vendor_eval_deps "$runtime_dir" >&2 || install_rc=$? + if [ "$install_rc" -ne 0 ]; then + rm -rf "$runtime_dir" + return "$install_rc" + fi + printf '%s\n' "$runtime_dir" +} + _prepare_kimi_vendor_verifier() { local repo_url="$1" local verifier_ref="$2" @@ -866,10 +878,17 @@ _prepare_kimi_vendor_verifier() { echo "ERROR: failed to fetch Kimi-Vendor-Verifier at ${verifier_ref}" >&2 return 1 fi - KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$checkout_dir" + printf '%s\n' "$checkout_dir" } -_write_tool_use_integration_error() { +_cleanup_kimi_vendor_eval() { + local path + for path in "$@"; do + [ -z "$path" ] || rm -rf "$path" || true + done +} + +_write_kimi_vendor_integration_error() { local adapter_path="$1" local model_name="$2" local results_dir="$3" @@ -878,11 +897,10 @@ _write_tool_use_integration_error() { python3 "$adapter_path" \ --model "$model_name" \ --output-dir "$results_dir" \ - --integration-error "$message" \ - || true + --integration-error "$message" } -run_tool_use_eval() { +_run_kimi_tool_call_schema_eval() { local port="${PORT:-8888}" local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}" local verifier_repo="https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" @@ -908,68 +926,85 @@ run_tool_use_eval() { esac done - local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" - if [ "$eval_suite" != "kimi_tool_call_schema" ]; then - echo "ERROR: tool-use supports only EVAL_SUITE=kimi_tool_call_schema" >&2 - export EVAL_RESULT_DIR="" - return 2 - fi case "${IS_MULTINODE:-false}" in true|1) - echo "ERROR: tool-use Phase 1 supports single-node evals only" >&2 + echo "ERROR: Kimi tool-call schema eval supports single-node only" >&2 export EVAL_RESULT_DIR="" return 2 ;; esac - export EVAL_SUITE="$eval_suite" - local _repo_root - _repo_root="$(cd "$INFERENCEX_BENCHMARK_LIB_DIR/.." && pwd)" + local repo_root + repo_root="$(cd "$INFERENCEX_BENCHMARK_LIB_DIR/.." && pwd)" local model_name="${MODEL_NAME:-${MODEL:-}}" - local adapter_path="${_repo_root}/utils/evals/kimi_vendor_eval.py" + local adapter_path="${repo_root}/utils/evals/kimi_vendor_eval.py" + local runtime_dir="" + local checkout_dir="" mkdir -p "$results_dir" || return $? export EVAL_RESULT_DIR="$results_dir" local setup_rc=0 integration_error="" - _require_tool_use_python || { + _require_kimi_vendor_python || { setup_rc=$? - integration_error="tool-use Python version check failed with exit code ${setup_rc}" + integration_error="Kimi Vendor Verifier Python version check failed with exit code ${setup_rc}" } - if [ "$setup_rc" -eq 0 ] \ - && [ "${INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY:-false}" != "true" ]; then - if _install_tool_use_eval_deps; then - export INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY=true - else + if [ "$setup_rc" -eq 0 ]; then + runtime_dir=$(_prepare_kimi_vendor_runtime) || { setup_rc=$? - integration_error="tool-use dependency installation failed with exit code ${setup_rc}" - fi + integration_error="Kimi Vendor Verifier dependency installation failed with exit code ${setup_rc}" + } fi if [ "$setup_rc" -eq 0 ]; then - _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref" || { + checkout_dir=$( + _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref" + ) || { setup_rc=$? - integration_error="tool-use verifier checkout failed with exit code ${setup_rc}" + integration_error="Kimi Vendor Verifier checkout failed with exit code ${setup_rc}" } fi if [ "$setup_rc" -ne 0 ]; then + _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" echo "ERROR: ${integration_error}" >&2 - _write_tool_use_integration_error \ - "$adapter_path" "$model_name" "$results_dir" "$integration_error" + local artifact_rc=0 + _write_kimi_vendor_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write Kimi verifier failure artifact (exit code ${artifact_rc})" >&2 + fi return "$setup_rc" fi local eval_rc=0 - python3 "$adapter_path" \ - --verifier-dir "$KIMI_VENDOR_VERIFIER_CHECKOUT_DIR" \ - --base-url "http://127.0.0.1:${port}/v1" \ - --api-key EMPTY \ - --model "$model_name" \ - --output-dir "$results_dir" \ - || eval_rc=$? - rm -rf "$KIMI_VENDOR_VERIFIER_CHECKOUT_DIR" || true + PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ + python3 "$adapter_path" \ + --verifier-dir "$checkout_dir" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --api-key EMPTY \ + --model "$model_name" \ + --output-dir "$results_dir" \ + || eval_rc=$? + _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" return "$eval_rc" } +run_kimi_vendor_eval() { + local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" + export EVAL_SUITE="$eval_suite" + + case "$eval_suite" in + kimi_tool_call_schema) + _run_kimi_tool_call_schema_eval "$@" + ;; + *) + echo "ERROR: unsupported Kimi Vendor Verifier suite '${eval_suite}'" >&2 + export EVAL_RESULT_DIR="" + return 2 + ;; + esac +} + _eval_patches_dir() { cd "$(dirname "${BASH_SOURCE[0]}")/../utils/evals/patches" && pwd } @@ -1728,9 +1763,9 @@ run_eval() { local framework="${EVAL_FRAMEWORK:-${cli_framework:-$scenario_default}}" - # Tool-use uses the verifier's fixed request budget and does not consume + # Kimi Vendor Verifier uses a fixed request budget and does not consume # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for that path. - if [ "$framework" != "tool-use" ] && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then + if [ "$framework" != "kimi-vendor" ] && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then compute_eval_context_length "$MODEL" "${MAX_MODEL_LEN:-0}" > /dev/null fi @@ -1801,7 +1836,7 @@ run_eval() { case "$framework" in lm-eval|lm_eval) run_lm_eval "${forwarded[@]}" || eval_rc=$? ;; swebench) run_swebench_eval "${forwarded[@]}" || eval_rc=$? ;; - tool-use) run_tool_use_eval "${forwarded[@]}" || eval_rc=$? ;; + kimi-vendor) run_kimi_vendor_eval "${forwarded[@]}" || eval_rc=$? ;; *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index fd7c0b1d05..1070a9305e 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -32,6 +32,7 @@ SPEC_DECODING = "Spec Decode" CONC_SUFFIX_RE = re.compile(r"_conc(\d+)(?:_\d+)?\.json$") +EVAL_RESULT_FORMAT = "inferencex-eval-v1" def load_json(path: Path) -> Optional[Dict[str, Any]]: @@ -71,10 +72,10 @@ def result_concurrency(path: Path) -> Optional[int]: def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: - """Return lm-eval result JSONs from one artifact directory. + """Return collector-compatible eval result JSONs from one artifact directory. - Legacy artifacts contribute their latest result file. Batched artifacts - contribute the latest result file for each `_concN` suffix. + Legacy lm-eval artifacts contribute their latest result file. Batched + artifacts contribute the latest result file for each `_concN` suffix. """ immediate_jsons = set(d.glob('results*.json')) immediate_jsons.update( @@ -86,7 +87,7 @@ def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: data = load_json(p) if not isinstance(data, dict): continue - if 'lm_eval_version' in data: + if data.get('result_format') == EVAL_RESULT_FORMAT or 'lm_eval_version' in data: lm_paths.append(p) if not lm_paths: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index b85059e415..dcad93c066 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -44,14 +44,21 @@ runner. Existing jobs continue to use lm-eval with GSM8K by default. The default eval framework is [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (`lm-eval`). Agentic eval-only matrix jobs inherit this default and therefore run the same GSM8K task as 8k1k. Explicit agentic runs can still select SWE-bench. -The Phase 1 tool-use smoke is opt-in and single-node only. Select it with the -`eval-framework: tool-use` input on `e2e-tests.yml`, or invoke it after a server -is ready: +The Phase 1 Kimi smoke is opt-in and single-node only. Select +`eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema` on +`e2e-tests.yml`, or invoke it after a server is ready: ```bash -EVAL_FRAMEWORK=tool-use run_eval --port "$PORT" +EVAL_FRAMEWORK=kimi-vendor EVAL_SUITE=kimi_tool_call_schema \ + run_eval --port "$PORT" ``` +The framework selects a provider-specific subprocess adapter, while the suite +selects a case set understood by that adapter. Each adapter owns its endpoint +format, dependencies, native report, metrics, and pass policy. Future MiniMax +or BFCL support should add explicit `run_eval` cases rather than a shared +request or report abstraction. + ### Stock Kimi tool-call schema smoke The smoke runs the unmodified @@ -62,7 +69,8 @@ tests, and bundled Walle cases. InferenceX does not install the verifier package or reimplement its request, streaming, retry, or validation logic. Python 3.12 or newer is required. The runner installs the minimal pinned runtime -(`httpx[http2]`, `openai`, `jsonschema`, and `pytest`), then runs upstream +(`httpx[http2]`, `openai`, `jsonschema`, and `pytest`) into a temporary isolated +package directory, then runs upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; @@ -73,8 +81,9 @@ The selection is `TestAdditionalProperties:1`, parametrized upstream in non-streaming and streaming modes. The unchanged native report is uploaded as `kimi_vendor_report.json`. `utils/evals/kimi_vendor_eval.py` only projects its two outcomes into the existing eval result shape. Both must pass, so the -`kimi_tool_call_schema` threshold is `1.0`. Setup and collection failures emit a -zero-score result with error metadata. +`kimi_tool_call_schema` threshold is `1.0`. Setup, timeout, and collection +failures emit a zero-score result with error metadata. The adapter bounds the +upstream pytest process to 900 seconds. This smoke validates one object-schema tool call. It does not cover tool choice, parallel calls, multi-turn execution, or general agent quality. Multi-value @@ -109,10 +118,10 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: |----------|-------------| | `run_eval` | Unified entrypoint - dispatches to framework-specific runner | | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | -| `run_tool_use_eval` | Runs the pinned stock verifier in non-stream and stream modes | +| `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | -| `_install_tool_use_eval_deps` | Installs the minimal pinned stock-verifier runtime | +| `_prepare_kimi_vendor_runtime` | Installs the minimal pinned runtime in an isolated temp path | | `_prepare_kimi_vendor_verifier` | Fetches a fresh pinned sparse checkout | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | @@ -189,8 +198,8 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' |----------|---------|-------------| | `RUN_EVAL` | `false` | Enable eval after throughput benchmark | | `EVAL_ONLY` | `false` | Skip throughput, only run evals (set by workflow) | -| `EVAL_FRAMEWORK` | `lm-eval` | Eval framework to use | -| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Eval suite metadata; explicit values take precedence | +| `EVAL_FRAMEWORK` | `lm-eval` | Eval runner (`lm-eval`, `swebench`, or `kimi-vendor`) | +| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Runner-specific suite selector and artifact identity; the workflow `eval-suite` input sets it explicitly | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | @@ -206,6 +215,15 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' 2. Set `EVAL_TASKS_DIR=utils/evals/.yaml` when running benchmarks. 3. Update `utils/collect_eval_results.py` if new metrics need extraction. +### Adding a provider verifier + +1. Add a provider-specific adapter under `utils/evals/`. +2. Add an explicit framework case in `run_eval`; keep suite-specific policy in + that adapter's shell runner. +3. Install dependencies in a provider-specific isolated runtime. +4. Emit `result_format: inferencex-eval-v1`, preserve the native report as + `*_vendor_report.json`, set `EVAL_SUITE`, and add a threshold. + ### Runtime patches (`utils/evals/patches/`) The benchmark helpers invoke these standalone scripts against pinned dependencies. diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index a71845cc41..01d024771c 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -14,8 +14,11 @@ TASK_NAME = "kimi_tool_call_schema" NATIVE_REPORT_FILENAME = "kimi_vendor_report.json" -COMPATIBILITY_GLOB = "results_*.json" +COMPATIBILITY_GLOB = "results_kimi_vendor_*.json" EXPECTED_MODES = {"non-stream", "stream"} +DEFAULT_TIMEOUT_SECONDS = 900 +RESULT_FORMAT = "inferencex-eval-v1" +ADAPTER_NAME = "kimi-vendor-verifier" def prepare_compatibility_path(output_dir: Path) -> Path: @@ -23,7 +26,7 @@ def prepare_compatibility_path(output_dir: Path) -> Path: for stale_path in output_dir.glob(COMPATIBILITY_GLOB): stale_path.unlink() timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") - return output_dir / f"results_{timestamp}.json" + return output_dir / f"results_kimi_vendor_{timestamp}.json" def build_pytest_command( @@ -111,7 +114,8 @@ def _compatibility_result( model: str, score: float, integration_error: BaseException | None = None ) -> dict[str, Any]: result: dict[str, Any] = { - "lm_eval_version": "kimi-vendor-verifier", + "result_format": RESULT_FORMAT, + "eval_adapter": ADAPTER_NAME, "model_name": model, "results": { TASK_NAME: { @@ -146,6 +150,7 @@ def run_evaluation( api_key: str, model: str, output_dir: Path, + timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -167,11 +172,18 @@ def run_evaluation( ), cwd=verifier_dir, check=False, + timeout=timeout_seconds, ) subprocess_rc = completed.returncode report = json.loads(native_report.read_text(encoding="utf-8")) compatibility, complete_pass = _project_report(model, report) - except (OSError, ValueError, json.JSONDecodeError) as exc: + if subprocess_rc != 0 and complete_pass: + integration_error = RuntimeError( + f"upstream verifier exited with code {subprocess_rc}" + ) + compatibility = _compatibility_result(model, 0.0, integration_error) + complete_pass = False + except (OSError, ValueError, subprocess.TimeoutExpired) as exc: integration_error = exc compatibility = _compatibility_result(model, 0.0, exc) finally: @@ -185,6 +197,13 @@ def run_evaluation( return subprocess_rc == 0 and complete_pass and integration_error is None +def _positive_int(value: str) -> int: + parsed = int(value) + if parsed < 1: + raise argparse.ArgumentTypeError("must be a positive integer") + return parsed + + def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run the pinned stock Kimi Vendor Verifier tool-schema smoke test." @@ -194,6 +213,9 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--api-key", default="EMPTY") parser.add_argument("--model", required=True) parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument( + "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS + ) parser.add_argument("--integration-error") args = parser.parse_args(argv) if args.integration_error is None: @@ -230,6 +252,7 @@ def main(argv: Sequence[str] | None = None) -> int: api_key=args.api_key, model=args.model, output_dir=args.output_dir, + timeout_seconds=args.timeout_seconds, ) return 0 if passed else 1 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index ff223d3a35..db08ce17d9 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -29,16 +29,14 @@ def _result(output_dir: Path) -> dict[str, Any]: paths = list(output_dir.glob(kve.COMPATIBILITY_GLOB)) assert len(paths) == 1 assert re.fullmatch( - r"results_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", + r"results_kimi_vendor_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", paths[0].name, ) return json.loads(paths[0].read_text()) def _score(output_dir: Path) -> float: - return _result(output_dir)["results"][kve.TASK_NAME][ - "exact_match,strict-match" - ] + return _result(output_dir)["results"][kve.TASK_NAME]["exact_match,strict-match"] def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: @@ -77,7 +75,11 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: @pytest.mark.parametrize( ("stream_status", "return_code", "expected_pass", "expected_score"), - (("passed", 0, True, 1.0), ("failed", 1, False, 0.5)), + ( + ("passed", 0, True, 1.0), + ("passed", 1, False, 0.0), + ("failed", 1, False, 0.5), + ), ) def test_projects_upstream_outcomes( tmp_path: Path, @@ -91,11 +93,11 @@ def test_projects_upstream_outcomes( native_bytes = json.dumps(_report(stream_status)).encode() invocation: dict[str, Any] = {} - def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: - invocation.update(command=command, cwd=cwd, check=check) - Path(command[command.index("--tool-json-report") + 1]).write_bytes( - native_bytes - ) + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + invocation.update(command=command, cwd=cwd, check=check, timeout=timeout) + Path(command[command.index("--tool-json-report") + 1]).write_bytes(native_bytes) return SimpleNamespace(returncode=return_code) monkeypatch.setattr(kve.subprocess, "run", fake_run) @@ -112,7 +114,12 @@ def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: ) assert invocation["cwd"] == tmp_path assert invocation["check"] is False + assert invocation["timeout"] == kve.DEFAULT_TIMEOUT_SECONDS assert _score(output_dir) == expected_score + projected = _result(output_dir) + assert projected["result_format"] == kve.RESULT_FORMAT + assert projected["eval_adapter"] == kve.ADAPTER_NAME + assert "lm_eval_version" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes @@ -122,21 +129,25 @@ def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: (None, "FileNotFoundError"), ("{bad-json", "JSONDecodeError"), (OSError("boom"), "OSError"), + ( + subprocess.TimeoutExpired("pytest", kve.DEFAULT_TIMEOUT_SECONDS), + "TimeoutExpired", + ), ), ) def test_collection_failures_write_zero_score( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, - failure: str | OSError | None, + failure: str | BaseException | None, error_type: str, ) -> None: - def fake_run(command: list[str], *, cwd: Path, check: bool) -> SimpleNamespace: - if isinstance(failure, OSError): + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + if isinstance(failure, BaseException): raise failure if failure is not None: - Path(command[command.index("--tool-json-report") + 1]).write_text( - failure - ) + Path(command[command.index("--tool-json-report") + 1]).write_text(failure) return SimpleNamespace(returncode=1) monkeypatch.setattr(kve.subprocess, "run", fake_run) @@ -161,7 +172,11 @@ def test_failure_cannot_reuse_stale_outputs( output_dir.mkdir() native_report = output_dir / kve.NATIVE_REPORT_FILENAME native_report.write_text(json.dumps(_report())) - (output_dir / "results_2000-01-01T00-00-00.000000.json").write_text("{}") + (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( + "{}" + ) + foreign_result = output_dir / "results_other_eval.json" + foreign_result.write_text("{}") def fail_collection(*args: Any, **kwargs: Any) -> SimpleNamespace: assert not native_report.exists() @@ -178,25 +193,31 @@ def fail_collection(*args: Any, **kwargs: Any) -> SimpleNamespace: ) assert _score(output_dir) == 0.0 assert not native_report.exists() + assert foreign_result.exists() def test_cli_setup_failure_clears_stale_outputs(tmp_path: Path) -> None: output_dir = tmp_path / "output" output_dir.mkdir() (output_dir / kve.NATIVE_REPORT_FILENAME).write_text(json.dumps(_report())) - (output_dir / "results_2000-01-01T00-00-00.000000.json").write_text("{}") - - assert kve.main( - [ - "--model", - "model-a", - "--output-dir", - str(output_dir), - "--integration-error", - "checkout failed", - ] - ) == 1 + (output_dir / "results_kimi_vendor_2000-01-01T00-00-00.000000.json").write_text( + "{}" + ) + + assert ( + kve.main( + [ + "--model", + "model-a", + "--output-dir", + str(output_dir), + "--integration-error", + "checkout failed", + ] + ) + == 1 + ) projected = _result(output_dir) assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() assert _score(output_dir) == 0.0 - assert projected["integration_error"]["message"] == "checkout failed" \ No newline at end of file + assert projected["integration_error"]["message"] == "checkout failed" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1f6478934a..62d0ebaad2 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -14,7 +14,7 @@ source "$BENCHMARK_LIB" run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } -run_tool_use_eval() { echo "DISPATCH=tool-use"; } +run_kimi_vendor_eval() { echo "DISPATCH=kimi-vendor"; } append_lm_eval_summary() { echo "STAGED=summary"; } export EVAL_MAX_MODEL_LEN=16384 export EVAL_CONCURRENT_REQUESTS="" @@ -82,21 +82,21 @@ def test_env_can_force_swebench_on_fixed_seqlen(): -def test_env_can_force_tool_use_on_agentic_eval() -> None: - assert "DISPATCH=tool-use" in _dispatch( +def test_env_can_force_kimi_vendor_on_agentic_eval() -> None: + assert "DISPATCH=kimi-vendor" in _dispatch( is_agentic="1", eval_only="true", - env_fw="tool-use", + env_fw="kimi-vendor", ) -def test_tool_use_skips_unused_model_context_loading() -> None: +def test_kimi_vendor_skips_unused_model_context_loading() -> None: script = r''' source "$BENCHMARK_LIB" unset EVAL_MAX_MODEL_LEN compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } -run_tool_use_eval() { echo "DISPATCH=tool-use"; } -export EVAL_FRAMEWORK=tool-use +run_kimi_vendor_eval() { echo "DISPATCH=kimi-vendor"; } +export EVAL_FRAMEWORK=kimi-vendor export EVAL_CONCURRENT_REQUESTS="" export EVAL_ONLY=false export IS_AGENTIC=0 @@ -111,7 +111,7 @@ def test_tool_use_skips_unused_model_context_loading() -> None: ) assert result.returncode == 0, result.stderr - assert "DISPATCH=tool-use" in result.stdout + assert "DISPATCH=kimi-vendor" in result.stdout assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout @@ -143,43 +143,43 @@ def test_run_eval_rejects_missing_framework_value(): assert "--framework requires a value" in result.stderr -def test_tool_use_rejects_batched_concurrency() -> None: +def test_kimi_vendor_rejects_batched_concurrency() -> None: result = _run_invalid_call( "EVAL_MAX_MODEL_LEN=16384 " "EVAL_CONCURRENT_REQUESTS='1 4' " - "run_eval --framework tool-use" + "run_eval --framework kimi-vendor" ) assert result.returncode == 1 assert "batched eval concurrency is only supported for lm-eval" in result.stderr -def test_tool_use_rejects_unsupported_suite() -> None: +def test_kimi_vendor_rejects_unsupported_suite() -> None: result = _run_invalid_call( - "EVAL_SUITE=gsm8k run_tool_use_eval" + "EVAL_SUITE=gsm8k run_kimi_vendor_eval" ) assert result.returncode == 2 - assert "supports only EVAL_SUITE=kimi_tool_call_schema" in result.stderr + assert "unsupported Kimi Vendor Verifier suite 'gsm8k'" in result.stderr -def test_tool_use_rejects_multinode() -> None: +def test_kimi_vendor_rejects_multinode() -> None: for value in ("true", "1"): result = _run_invalid_call( f"EVAL_SUITE=kimi_tool_call_schema IS_MULTINODE={value} " - "run_tool_use_eval" + "run_kimi_vendor_eval" ) assert result.returncode == 2 - assert "supports single-node evals only" in result.stderr + assert "supports single-node only" in result.stderr -def test_tool_use_setup_failure_writes_compatibility_result( +def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, ) -> None: results_dir = tmp_path / "results" script = r''' source "$BENCHMARK_LIB" -_require_tool_use_python() { :; } -_install_tool_use_eval_deps() { return 12; } -run_tool_use_eval --results-dir "$RESULTS_DIR" +_require_kimi_vendor_python() { :; } +_prepare_kimi_vendor_runtime() { return 12; } +run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'SETUP_RC=%s\n' "$?" ''' env = { @@ -193,7 +193,6 @@ def test_tool_use_setup_failure_writes_compatibility_result( for key in ( "EVAL_SUITE", "EVAL_RESULT_DIR", - "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", "MODEL_NAME", ): env.pop(key, None) @@ -205,7 +204,7 @@ def test_tool_use_setup_failure_writes_compatibility_result( capture_output=True, check=True, ) - message = "tool-use dependency installation failed with exit code 12" + message = "Kimi Vendor Verifier dependency installation failed with exit code 12" score_files = list(results_dir.glob("results*.json")) assert "SETUP_RC=12" in result.stdout @@ -222,24 +221,79 @@ def test_tool_use_setup_failure_writes_compatibility_result( assert not (results_dir / "kimi_vendor_report.json").exists() +def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: + runtime_dir = tmp_path / "runtime" + script = r''' +source "$BENCHMARK_LIB" +python3() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +_install_kimi_vendor_eval_deps "$RUNTIME_DIR" +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RUNTIME_DIR": str(runtime_dir), + }, + text=True, + capture_output=True, + check=True, + ) + + assert "PYTHON_ARG=<--target>" in result.stdout + assert f"PYTHON_ARG=<{runtime_dir}>" in result.stdout + assert "--break-system-packages" not in result.stdout -def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: +def test_kimi_vendor_surfaces_failure_artifact_error(tmp_path: Path) -> None: + script = r''' +source "$BENCHMARK_LIB" +_require_kimi_vendor_python() { return 12; } +_write_kimi_vendor_integration_error() { return 23; } +run_kimi_vendor_eval --results-dir "$RESULTS_DIR" +printf 'EVAL_RC=%s\n' "$?" +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(tmp_path / "results"), + "MODEL": "test-model", + "IS_MULTINODE": "false", + }, + text=True, + capture_output=True, + check=True, + ) + + assert "EVAL_RC=12" in result.stdout + assert "failed to write Kimi verifier failure artifact" in result.stderr + + + + +def test_kimi_vendor_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: results_dir = tmp_path / "results" verifier_dir = tmp_path / "verifier" + runtime_dir = tmp_path / "runtime" + runtime_dir.mkdir() + verifier_dir.mkdir() script = r''' source "$BENCHMARK_LIB" -_require_tool_use_python() { :; } -_install_tool_use_eval_deps() { echo "INSTALL=UPSTREAM_MINIMAL"; } +_require_kimi_vendor_python() { :; } +_prepare_kimi_vendor_runtime() { printf '%s\n' "$RUNTIME_DIR"; } _prepare_kimi_vendor_verifier() { - printf 'CHECKOUT=%s@%s\n' "$1" "$2" - KIMI_VENDOR_VERIFIER_CHECKOUT_DIR="$VERIFIER_DIR" + printf 'CHECKOUT=%s@%s\n' "$1" "$2" >&2 + printf '%s\n' "$VERIFIER_DIR" } -python3() { printf 'PYTHON_ARG=<%s>\n' "$@"; } -run_tool_use_eval --port 9999 --results-dir "$RESULTS_DIR" +python3() { + printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" + printf 'PYTHON_ARG=<%s>\n' "$@" +} +run_kimi_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" -printf 'RUNTIME_READY=%s\n' "$INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY" ''' env = { **os.environ, @@ -247,6 +301,7 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: "RESULTS_DIR": str(results_dir), "VERIFIER_DIR": str(verifier_dir), "MODEL": "test-model", + "RUNTIME_DIR": str(runtime_dir), "OPENAI_API_KEY": "must-not-be-forwarded", "KV_OFFLOADING": "none", "IS_MULTINODE": "false", @@ -254,8 +309,6 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: for key in ( "EVAL_SUITE", "EVAL_RESULT_DIR", - "INFERENCEX_TOOL_USE_EVAL_RUNTIME_READY", - "KIMI_VENDOR_VERIFIER_CHECKOUT_DIR", "MODEL_NAME", ): env.pop(key, None) @@ -267,10 +320,10 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: capture_output=True, check=True, ) - output = result.stdout + output = result.stdout + result.stderr adapter = BENCHMARK_LIB.parents[1] / "utils/evals/kimi_vendor_eval.py" - assert output.count("INSTALL=UPSTREAM_MINIMAL") == 1 + assert f"PYTHONPATH=<{tmp_path / 'runtime'}" in output assert ( "CHECKOUT=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" "@b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" @@ -287,7 +340,8 @@ def test_tool_use_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: assert "must-not-be-forwarded" not in output assert "EVAL_SUITE=kimi_tool_call_schema" in output assert f"EVAL_RESULT_DIR={results_dir}" in output - assert "RUNTIME_READY=true" in output + assert not (tmp_path / "runtime").exists() + assert not verifier_dir.exists() def test_run_lm_eval_rejects_missing_option_value(): diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index ba9ebef44c..3842aeae4c 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from collect_eval_results import build_row, collect_eval_rows +from collect_eval_results import EVAL_RESULT_FORMAT, build_row, collect_eval_rows def test_build_row_preserves_sequence_lengths() -> None: @@ -119,3 +119,24 @@ def test_collect_eval_rows_ignores_failed_batch_points( rows = collect_eval_rows(tmp_path) assert [row["conc"] for row in rows] == [4] + + + +def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None: + artifact_dir = tmp_path / "eval_provider" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "provider_smoke"}) + ) + result_path = artifact_dir / "results_provider.json" + _write_lm_eval_result(result_path, 1.0) + result = json.loads(result_path.read_text()) + result.pop("lm_eval_version") + result["result_format"] = EVAL_RESULT_FORMAT + result_path.write_text(json.dumps(result)) + + rows = collect_eval_rows(tmp_path) + + assert len(rows) == 1 + assert rows[0]["score"] == 1.0 + assert rows[0]["eval_suite"] == "provider_smoke" \ No newline at end of file From b0fd8cc53c97b415197795825cb8a22f99e343b3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:50:12 -0500 Subject: [PATCH 04/99] fix: distinguish successful failure artifact writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:区分失败结果产物写入成功与写入失败,同时保留原始安装失败退出码。 --- utils/evals/kimi_vendor_eval.py | 2 +- utils/evals/test_kimi_vendor_eval.py | 4 ++-- utils/evals/test_run_eval_dispatch.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 01d024771c..3f183c5423 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -245,7 +245,7 @@ def main(argv: Sequence[str] | None = None) -> int: args.model, 0.0, RuntimeError(args.integration_error) ), ) - return 1 + return 0 passed = run_evaluation( verifier_dir=args.verifier_dir, base_url=args.base_url, diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index db08ce17d9..fa126e3134 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -196,7 +196,7 @@ def fail_collection(*args: Any, **kwargs: Any) -> SimpleNamespace: assert foreign_result.exists() -def test_cli_setup_failure_clears_stale_outputs(tmp_path: Path) -> None: +def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: output_dir = tmp_path / "output" output_dir.mkdir() (output_dir / kve.NATIVE_REPORT_FILENAME).write_text(json.dumps(_report())) @@ -215,7 +215,7 @@ def test_cli_setup_failure_clears_stale_outputs(tmp_path: Path) -> None: "checkout failed", ] ) - == 1 + == 0 ) projected = _result(output_dir) assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 62d0ebaad2..e670b2fc2d 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -209,6 +209,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( assert "SETUP_RC=12" in result.stdout assert message in result.stderr + assert "failed to write Kimi verifier failure artifact" not in result.stderr assert len(score_files) == 1 score_result = json.loads(score_files[0].read_text()) assert ( From 1684f55273309fb68816d84f17c3e29fc3c771d3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:02:42 -0500 Subject: [PATCH 05/99] fix: preserve agentic eval decoding mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将 agentic 评估矩阵的投机解码模式传递给启动器,避免 MTP 配置静默回退到 STP。 --- .github/workflows/e2e-tests.yml | 2 +- utils/evals/EVALS.md | 2 ++ utils/evals/test_run_eval_dispatch.py | 14 +++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6b9c67410b..b96cf4eab8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -473,7 +473,7 @@ jobs: isl: '0' osl: '0' max-model-len: '0' - spec-decoding: 'none' + spec-decoding: ${{ matrix.config.spec-decoding }} disagg: ${{ 'false' }} run-eval: true eval-only: true diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index dcad93c066..b42316ecac 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -58,6 +58,8 @@ selects a case set understood by that adapter. Each adapter owns its endpoint format, dependencies, native report, metrics, and pass policy. Future MiniMax or BFCL support should add explicit `run_eval` cases rather than a shared request or report abstraction. +Agentic eval jobs forward the matrix `spec-decoding` value, so MTP entries +launch their existing `*_mtp.sh` server instead of silently falling back to STP. ### Stock Kimi tool-call schema smoke diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index e670b2fc2d..cad7cb09ad 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -7,8 +7,11 @@ import subprocess from pathlib import Path +import yaml -BENCHMARK_LIB = Path(__file__).resolve().parents[2] / "benchmarks" / "benchmark_lib.sh" +REPO_ROOT = Path(__file__).resolve().parents[2] +BENCHMARK_LIB = REPO_ROOT / "benchmarks" / "benchmark_lib.sh" +E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" _SCRIPT = r''' source "$BENCHMARK_LIB" @@ -1001,3 +1004,12 @@ def test_eval_limit_full_and_zero_accepted(tmp_path): assert "GEN_RC=0" in res.stdout, f"EVAL_LIMIT={sentinel!r}: {res.stdout}{res.stderr}" argv = (shim / "argv.log").read_text() assert "--slice" not in argv + + +def test_agentic_eval_workflow_forwards_runner_contract() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + forwarded = workflow["jobs"]["test-sweep-agentic-evals"]["with"] + + assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" + assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" + assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" From 58858593f73219274224ef73537da445c249e36a Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:31:22 -0500 Subject: [PATCH 06/99] fix: complete eval-only workflow runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:补全纯评估工作流的结果收集依赖,避免所有评估任务成功后工作流仍显示失败。 --- .github/workflows/e2e-tests.yml | 4 ++-- utils/evals/EVALS.md | 2 +- utils/evals/test_run_eval_dispatch.py | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b96cf4eab8..91b3d57e53 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -618,8 +618,8 @@ jobs: ref: ${{ inputs.ref }} collect-results: - needs: [test-sweep-multi-node, test-sweep-single-node, test-sweep-agentic, test-sweep-multi-node-agentic] - if: ${{ always() && (needs.test-sweep-multi-node.result != 'skipped' || needs.test-sweep-single-node.result != 'skipped' || needs.test-sweep-agentic.result != 'skipped' || needs.test-sweep-multi-node-agentic.result != 'skipped') }} + needs: [test-sweep-multi-node, test-sweep-single-node, test-sweep-agentic, test-sweep-multi-node-agentic, test-sweep-evals, test-sweep-multi-node-evals, test-sweep-agentic-evals] + if: ${{ always() && (needs.test-sweep-multi-node.result != 'skipped' || needs.test-sweep-single-node.result != 'skipped' || needs.test-sweep-agentic.result != 'skipped' || needs.test-sweep-multi-node-agentic.result != 'skipped' || needs.test-sweep-evals.result != 'skipped' || needs.test-sweep-multi-node-evals.result != 'skipped' || needs.test-sweep-agentic-evals.result != 'skipped') }} uses: ./.github/workflows/collect-results.yml secrets: inherit with: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index b42316ecac..0af37d1f4e 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -156,7 +156,7 @@ For multi-node `all-evals`, `EVAL_CONC` is a space-separated list. When it conta - `e2e-tests.yml`: `test-sweep-evals` (single-node) and `test-sweep-multi-node-evals` (multi-node) - `run-sweep.yml`: `sweep-evals` (single-node) and `sweep-multi-node-evals` (multi-node) - Both use their respective benchmark templates with `eval-only: true`, `run-eval: true` -- `collect-evals` depends on both eval jobs, while `collect-results` only runs when benchmark jobs ran +- `collect-evals` depends on the eval jobs. `run-sweep.yml` collects throughput results only when benchmark jobs ran; `e2e-tests.yml` also completes the throughput collector dependency after an eval-only dispatch so the workflow can finish successfully. - `process_changelog.py` splits eval results into `evals` (single-node) and `multinode_evals` ### Result collection diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index cad7cb09ad..0129a69e83 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1013,3 +1013,16 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + + +def test_eval_only_workflow_completes_throughput_collection_dependency() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + collect_results = workflow["jobs"]["collect-results"] + + for eval_job in ( + "test-sweep-evals", + "test-sweep-multi-node-evals", + "test-sweep-agentic-evals", + ): + assert eval_job in collect_results["needs"] + assert f"needs.{eval_job}.result != 'skipped'" in collect_results["if"] From 83d7f575664f1331c62ccd5dd423c43ffb093798 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:43:20 -0500 Subject: [PATCH 07/99] revert: keep eval-only collection scoped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collector dependency change did not affect GitHub’s workflow conclusion and added unrelated orchestration scope. 中文:恢复纯评估结果收集的原有范围。该依赖调整未改变 GitHub 工作流结论,且扩大了无关改动范围。 --- .github/workflows/e2e-tests.yml | 4 ++-- utils/evals/EVALS.md | 2 +- utils/evals/test_run_eval_dispatch.py | 13 ------------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 91b3d57e53..b96cf4eab8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -618,8 +618,8 @@ jobs: ref: ${{ inputs.ref }} collect-results: - needs: [test-sweep-multi-node, test-sweep-single-node, test-sweep-agentic, test-sweep-multi-node-agentic, test-sweep-evals, test-sweep-multi-node-evals, test-sweep-agentic-evals] - if: ${{ always() && (needs.test-sweep-multi-node.result != 'skipped' || needs.test-sweep-single-node.result != 'skipped' || needs.test-sweep-agentic.result != 'skipped' || needs.test-sweep-multi-node-agentic.result != 'skipped' || needs.test-sweep-evals.result != 'skipped' || needs.test-sweep-multi-node-evals.result != 'skipped' || needs.test-sweep-agentic-evals.result != 'skipped') }} + needs: [test-sweep-multi-node, test-sweep-single-node, test-sweep-agentic, test-sweep-multi-node-agentic] + if: ${{ always() && (needs.test-sweep-multi-node.result != 'skipped' || needs.test-sweep-single-node.result != 'skipped' || needs.test-sweep-agentic.result != 'skipped' || needs.test-sweep-multi-node-agentic.result != 'skipped') }} uses: ./.github/workflows/collect-results.yml secrets: inherit with: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 0af37d1f4e..b42316ecac 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -156,7 +156,7 @@ For multi-node `all-evals`, `EVAL_CONC` is a space-separated list. When it conta - `e2e-tests.yml`: `test-sweep-evals` (single-node) and `test-sweep-multi-node-evals` (multi-node) - `run-sweep.yml`: `sweep-evals` (single-node) and `sweep-multi-node-evals` (multi-node) - Both use their respective benchmark templates with `eval-only: true`, `run-eval: true` -- `collect-evals` depends on the eval jobs. `run-sweep.yml` collects throughput results only when benchmark jobs ran; `e2e-tests.yml` also completes the throughput collector dependency after an eval-only dispatch so the workflow can finish successfully. +- `collect-evals` depends on both eval jobs, while `collect-results` only runs when benchmark jobs ran - `process_changelog.py` splits eval results into `evals` (single-node) and `multinode_evals` ### Result collection diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 0129a69e83..cad7cb09ad 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1013,16 +1013,3 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" - - -def test_eval_only_workflow_completes_throughput_collection_dependency() -> None: - workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) - collect_results = workflow["jobs"]["collect-results"] - - for eval_job in ( - "test-sweep-evals", - "test-sweep-multi-node-evals", - "test-sweep-agentic-evals", - ): - assert eval_job in collect_results["needs"] - assert f"needs.{eval_job}.result != 'skipped'" in collect_results["if"] From 07dc9d95bac5ccfd72a73e578589e0c3ac26bfcb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:13:44 -0500 Subject: [PATCH 08/99] fix: correct verifier failure metadata and links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:修正验证器失败样本数、共享格式契约、路径复用及双语文档链接。 --- benchmarks/benchmark_lib.sh | 12 +-- docs/eval-agentx-procedures.md | 20 ++--- docs/eval-agentx-procedures_zh.md | 20 ++--- utils/evals/kimi_vendor_eval.py | 25 ++++-- utils/evals/test_kimi_vendor_eval.py | 7 ++ utils/test_collect_eval_results.py | 110 +++++++++++++++------------ 6 files changed, 114 insertions(+), 80 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 1c1c3d8f5f..4fc7ce6839 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -11,6 +11,9 @@ mkdir -p "$PYTHONPYCACHEPREFIX" 2>/dev/null || true INFERENCEX_BENCHMARK_LIB_DIR="$( cd "$(dirname "${BASH_SOURCE[0]}")" && pwd )" +INFERENCEX_REPO_ROOT="$( + cd "$INFERENCEX_BENCHMARK_LIB_DIR/.." && pwd +)" # Inference server port shared by every benchmark recipe. Launchers that need # a non-default value (e.g. launch_mi355x-amds.sh derives PORT from RUNNER_NAME @@ -934,10 +937,8 @@ _run_kimi_tool_call_schema_eval() { ;; esac - local repo_root - repo_root="$(cd "$INFERENCEX_BENCHMARK_LIB_DIR/.." && pwd)" local model_name="${MODEL_NAME:-${MODEL:-}}" - local adapter_path="${repo_root}/utils/evals/kimi_vendor_eval.py" + local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/kimi_vendor_eval.py" local runtime_dir="" local checkout_dir="" @@ -1006,7 +1007,7 @@ run_kimi_vendor_eval() { } _eval_patches_dir() { - cd "$(dirname "${BASH_SOURCE[0]}")/../utils/evals/patches" && pwd + printf '%s\n' "${INFERENCEX_REPO_ROOT}/utils/evals/patches" } _patch_lm_eval() { @@ -1115,8 +1116,7 @@ run_lm_eval() { done # Serving images may use a different WORKDIR. - local _repo_root - _repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + local _repo_root="$INFERENCEX_REPO_ROOT" if [[ "$tasks_dir" == *.yaml && "$tasks_dir" != /* \ && ! -f "$tasks_dir" && -f "$_repo_root/$tasks_dir" ]]; then echo "run_lm_eval: anchoring relative task '$tasks_dir' to repo root -> $_repo_root/$tasks_dir" diff --git a/docs/eval-agentx-procedures.md b/docs/eval-agentx-procedures.md index a3afb3076a..e5c349fd9c 100644 --- a/docs/eval-agentx-procedures.md +++ b/docs/eval-agentx-procedures.md @@ -85,7 +85,7 @@ append_lm_eval_summary python3 utils/evals/validate_scores.py --model-prefix "$MODEL_PREFIX" ``` -`run_lm_eval` passes concurrency through `num_concurrent` in `--model_args`. It is deliberately an environment variable, not a `run_eval` CLI option. The exact invocation is in [`run_lm_eval()`](../benchmarks/benchmark_lib.sh#L890-L970). +`run_lm_eval` passes concurrency through `num_concurrent` in `--model_args`. It is deliberately an environment variable, not a `run_eval` CLI option. The exact invocation is in [`run_lm_eval()`](../benchmarks/benchmark_lib.sh#L1080-L1162). ## 3. `EVAL_ONLY` is a launcher contract @@ -97,9 +97,9 @@ Set `EVAL_ONLY=true` **before server launch**. It is not merely a switch inside 4. Throughput returns immediately or is skipped. 5. `run_eval` and artifact staging run. -Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L853-L888), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1537-L1654), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L162-L185). +Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L1049-L1078), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1737-L1856), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L79-L97). -Do not toggle `EVAL_ONLY` after a throughput-sized server is already running and assume the context changed. Restart through the recipe. In eval-only mode an eval failure is returned after available artifacts are staged. In a workflow, upload happens with `always()` before score validation so failed evidence survives ([single-node upload and gate](../.github/workflows/benchmark-tmpl.yml#L387-L404), [multi-node upload and gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468)). +Do not toggle `EVAL_ONLY` after a throughput-sized server is already running and assume the context changed. Restart through the recipe. In eval-only mode an eval failure is returned after available artifacts are staged. In a workflow, upload happens with `always()` before score validation so failed evidence survives ([single-node upload and gate](../.github/workflows/benchmark-tmpl.yml#L399-L417), [multi-node upload and gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468)). ## 4. Batched eval concurrency @@ -121,9 +121,9 @@ The batch runner creates a fresh temporary output directory per point, stages fi - `completed_eval_concs`: eval and staging both succeeded. - `failed_eval_concs`: either eval or staging failed. -A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1537-L1631), [artifact suffixing](../benchmarks/benchmark_lib.sh#L972-L1030), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). +A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1737-L1832), [artifact suffixing](../benchmarks/benchmark_lib.sh#L1163-L1222), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). -For multi-node `all-evals`, the workflow constructs `EVAL_CONC` by joining the topology's concurrency list ([dispatch](../.github/workflows/e2e-tests.yml#L375-L378)). Never compare a point if its `_conc` result or completed-manifest entry is missing. +For multi-node `all-evals`, the workflow constructs `EVAL_CONC` by joining the topology's concurrency list ([dispatch](../.github/workflows/e2e-tests.yml#L394-L398)). Never compare a point if its `_conc` result or completed-manifest entry is missing. ## 5. Validate scores, not file existence @@ -173,7 +173,7 @@ Retain `meta_env.json`, `results*.json`, and `sample*.jsonl`. Agentic SWE-bench ## 7. Run AgentX: fast feedback versus canonical evidence -AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L1824-L1848)). +AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L2026-L2050)). Targeted canonical run (configured duration and warmup, with fast and duration overrides omitted): @@ -205,11 +205,11 @@ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ For a publishable SWE-bench score, omit `eval-limit`. Do not use `single-shot`, which is only a debugging escape hatch. SWE-bench generation/scoring controls and its `0.50` full-split threshold are documented next to the implementation in [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench). -Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L1982-L1989)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. +Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L2188-L2190)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. ## 8. Preserve trace and run provenance -AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L1743-L1822), [replay semantics](../benchmarks/benchmark_lib.sh#L1824-L1850)). +AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L1945-L2024), [replay semantics](../benchmarks/benchmark_lib.sh#L2026-L2192)). Capture orchestration provenance immediately: @@ -241,7 +241,7 @@ For each concurrency retain: - server/frontend logs and every metrics endpoint represented. - run URL/ID, attempt, head SHA, recipe/config identity, image, topology, fast flag, and any override. -The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2040-L2079)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L337-L346), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448)). +The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2242-L2282)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L349-L358), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448)). ## 9. Debug long AgentX runs from live evidence @@ -290,7 +290,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L1963-L1980)). +Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L2158-L2181)). Use phase markers, not total Slurm age: diff --git a/docs/eval-agentx-procedures_zh.md b/docs/eval-agentx-procedures_zh.md index 669ed290c3..904cc3d1b7 100644 --- a/docs/eval-agentx-procedures_zh.md +++ b/docs/eval-agentx-procedures_zh.md @@ -85,7 +85,7 @@ append_lm_eval_summary python3 utils/evals/validate_scores.py --model-prefix "$MODEL_PREFIX" ``` -`run_lm_eval` 通过 `--model_args` 中的 `num_concurrent` 传递并发;它刻意采用环境变量,而不是 `run_eval` CLI 选项。准确调用见 [`run_lm_eval()`](../benchmarks/benchmark_lib.sh#L890-L970)。 +`run_lm_eval` 通过 `--model_args` 中的 `num_concurrent` 传递并发;它刻意采用环境变量,而不是 `run_eval` CLI 选项。准确调用见 [`run_lm_eval()`](../benchmarks/benchmark_lib.sh#L1080-L1162)。 ## 3. `EVAL_ONLY` 是 launcher 约定 @@ -97,9 +97,9 @@ python3 utils/evals/validate_scores.py --model-prefix "$MODEL_PREFIX" 4. 吞吐量路径立即返回或被跳过。 5. 运行 `run_eval` 和 artifact staging。 -相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L853-L888)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1537-L1654) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L162-L185)。 +相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L1049-L1078)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1737-L1856) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L79-L97)。 -不要在吞吐量规格的服务已经运行后才切换 `EVAL_ONLY`,并假定 context 会随之变化。应通过 recipe 重启。Eval-only 模式会在暂存已有 artifact 后返回 eval 失败;在工作流中,上传步骤使用 `always()`,并位于分数校验前,因此失败证据仍会保留([单节点上传与 gate](../.github/workflows/benchmark-tmpl.yml#L387-L404)、[多节点上传与 gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468))。 +不要在吞吐量规格的服务已经运行后才切换 `EVAL_ONLY`,并假定 context 会随之变化。应通过 recipe 重启。Eval-only 模式会在暂存已有 artifact 后返回 eval 失败;在工作流中,上传步骤使用 `always()`,并位于分数校验前,因此失败证据仍会保留([单节点上传与 gate](../.github/workflows/benchmark-tmpl.yml#L399-L417)、[多节点上传与 gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468))。 ## 4. 批量 eval 并发 @@ -121,9 +121,9 @@ python3 utils/evals/validate_scores.py --expected-concs '16 32 64' - `completed_eval_concs`:eval 与 staging 均成功的点; - `failed_eval_concs`:eval 或 staging 失败的点。 -失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1537-L1631)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L972-L1030) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 +失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1737-L1832)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L1163-L1222) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 -对于多节点 `all-evals`,工作流通过连接拓扑的并发列表构造 `EVAL_CONC`([分派](../.github/workflows/e2e-tests.yml#L375-L378))。如果缺少某点的 `_conc` 结果或 completed manifest 条目,绝不能比较该点。 +对于多节点 `all-evals`,工作流通过连接拓扑的并发列表构造 `EVAL_CONC`([分派](../.github/workflows/e2e-tests.yml#L394-L398))。如果缺少某点的 `_conc` 结果或 completed manifest 条目,绝不能比较该点。 ## 5. 校验分数,而不只是检查文件存在 @@ -173,7 +173,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ ## 7. 运行 AgentX:快速反馈与 canonical 证据 -AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[Fast replay 设置](../benchmarks/benchmark_lib.sh#L1824-L1848))。 +AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[fast replay 设置](../benchmarks/benchmark_lib.sh#L2026-L2050))。 目标 canonical 运行(使用配置的 duration 和 warmup;不要加 fast 或 duration override): @@ -205,11 +205,11 @@ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ 要得到可发布的 SWE-bench 分数,省略 `eval-limit`;不要使用 `single-shot`,它只是诊断逃生选项。SWE-bench generation/scoring 控制项以及完整 split 的 `0.50` 阈值在实现旁的 [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench) 中说明。 -Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L1982-L1989))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 +Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L2188-L2190))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 ## 8. 保留 trace 与运行 provenance -AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L1743-L1822)、[replay 语义](../benchmarks/benchmark_lib.sh#L1824-L1850))。 +AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L1945-L2024)、[replay 语义](../benchmarks/benchmark_lib.sh#L2026-L2192))。 立即记录 orchestration provenance: @@ -241,7 +241,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ - server/frontend 日志以及所代表的每个 metrics endpoint; - run URL/ID、attempt、head SHA、recipe/config 标识、image、topology、fast 标志和所有 override。 -Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2040-L2079))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L337-L346)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448))。 +Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2242-L2282))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L349-L358)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448))。 ## 9. 用实时证据调试长时间 AgentX 运行 @@ -290,7 +290,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L1963-L1980))。 +通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L2158-L2181))。 应使用 phase marker,而不是 Slurm 总运行时间: diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 3f183c5423..f29c621c6a 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -107,11 +107,15 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: ): raise ValueError("report does not contain the expected stream modes") score = passed / 2.0 - return _compatibility_result(model, score), passed == 2 + return _compatibility_result(model, score, n_samples=2), passed == 2 def _compatibility_result( - model: str, score: float, integration_error: BaseException | None = None + model: str, + score: float, + *, + n_samples: int, + integration_error: BaseException | None = None, ) -> dict[str, Any]: result: dict[str, Any] = { "result_format": RESULT_FORMAT, @@ -129,7 +133,7 @@ def _compatibility_result( "filter_list": [{"name": "strict-match"}], } }, - "n-samples": {TASK_NAME: {"original": 2, "effective": 2}}, + "n-samples": {TASK_NAME: {"original": n_samples, "effective": n_samples}}, } if integration_error is not None: result["integration_error"] = { @@ -158,7 +162,7 @@ def run_evaluation( compatibility_path = prepare_compatibility_path(output_dir) subprocess_rc: int | None = None integration_error: BaseException | None = None - compatibility = _compatibility_result(model, 0.0) + compatibility = _compatibility_result(model, 0.0, n_samples=0) complete_pass = False try: @@ -181,11 +185,15 @@ def run_evaluation( integration_error = RuntimeError( f"upstream verifier exited with code {subprocess_rc}" ) - compatibility = _compatibility_result(model, 0.0, integration_error) + compatibility = _compatibility_result( + model, 0.0, n_samples=2, integration_error=integration_error + ) complete_pass = False except (OSError, ValueError, subprocess.TimeoutExpired) as exc: integration_error = exc - compatibility = _compatibility_result(model, 0.0, exc) + compatibility = _compatibility_result( + model, 0.0, n_samples=0, integration_error=exc + ) finally: try: _write_compatibility(compatibility_path, compatibility) @@ -242,7 +250,10 @@ def main(argv: Sequence[str] | None = None) -> int: _write_compatibility( prepare_compatibility_path(args.output_dir), _compatibility_result( - args.model, 0.0, RuntimeError(args.integration_error) + args.model, + 0.0, + n_samples=0, + integration_error=RuntimeError(args.integration_error), ), ) return 0 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index fa126e3134..fe8394efc8 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -39,6 +39,10 @@ def _score(output_dir: Path) -> float: return _result(output_dir)["results"][kve.TASK_NAME]["exact_match,strict-match"] +def _n_eff(output_dir: Path) -> int: + return _result(output_dir)["n-samples"][kve.TASK_NAME]["effective"] + + def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: report = tmp_path / kve.NATIVE_REPORT_FILENAME @@ -116,6 +120,7 @@ def fake_run( assert invocation["check"] is False assert invocation["timeout"] == kve.DEFAULT_TIMEOUT_SECONDS assert _score(output_dir) == expected_score + assert _n_eff(output_dir) == 2 projected = _result(output_dir) assert projected["result_format"] == kve.RESULT_FORMAT assert projected["eval_adapter"] == kve.ADAPTER_NAME @@ -163,6 +168,7 @@ def fake_run( projected = _result(output_dir) assert _score(output_dir) == 0.0 assert projected["integration_error"]["type"] == error_type + assert _n_eff(output_dir) == 0 def test_failure_cannot_reuse_stale_outputs( @@ -221,3 +227,4 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" + assert _n_eff(output_dir) == 0 diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 3842aeae4c..2c3b54a02a 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -4,6 +4,11 @@ from pathlib import Path from collect_eval_results import EVAL_RESULT_FORMAT, build_row, collect_eval_rows +from evals.kimi_vendor_eval import RESULT_FORMAT as KIMI_VENDOR_RESULT_FORMAT + + +def test_kimi_vendor_result_format_matches_collector_contract() -> None: + assert KIMI_VENDOR_RESULT_FORMAT == EVAL_RESULT_FORMAT def test_build_row_preserves_sequence_lengths() -> None: @@ -34,23 +39,27 @@ def test_build_row_preserves_explicit_eval_suite() -> None: def _write_lm_eval_result(path: Path, score: float) -> None: - path.write_text(json.dumps({ - "lm_eval_version": "0.4.0", - "model_name": "test-model", - "results": { - "gsm8k": { - "exact_match,strict-match": score, - "exact_match_stderr,strict-match": 0.01, - }, - }, - "configs": { - "gsm8k": { - "metric_list": [{"metric": "exact_match"}], - "filter_list": [{"name": "strict-match"}], - }, - }, - "n-samples": {"gsm8k": {"effective": 10}}, - })) + path.write_text( + json.dumps( + { + "lm_eval_version": "0.4.0", + "model_name": "test-model", + "results": { + "gsm8k": { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.01, + }, + }, + "configs": { + "gsm8k": { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], + }, + }, + "n-samples": {"gsm8k": {"effective": 10}}, + } + ) + ) def test_collect_eval_rows_expands_batched_concurrencies( @@ -58,27 +67,31 @@ def test_collect_eval_rows_expands_batched_concurrencies( ) -> None: artifact_dir = tmp_path / "eval_batch" artifact_dir.mkdir() - (artifact_dir / "meta_env.json").write_text(json.dumps({ - "is_multinode": True, - "infmax_model_prefix": "gptoss", - "hw": "gb200", - "framework": "dynamo-sglang", - "precision": "fp8", - "spec_decoding": "none", - "isl": 8192, - "osl": 1024, - "prefill_tp": 4, - "prefill_ep": 1, - "prefill_num_workers": 1, - "decode_tp": 8, - "decode_ep": 1, - "decode_num_workers": 2, - "eval_concs": [4, 16], - "completed_eval_concs": [4, 16], - "failed_eval_concs": [], - "conc": 4, - "eval_suite": "gsm8k", - })) + (artifact_dir / "meta_env.json").write_text( + json.dumps( + { + "is_multinode": True, + "infmax_model_prefix": "gptoss", + "hw": "gb200", + "framework": "dynamo-sglang", + "precision": "fp8", + "spec_decoding": "none", + "isl": 8192, + "osl": 1024, + "prefill_tp": 4, + "prefill_ep": 1, + "prefill_num_workers": 1, + "decode_tp": 8, + "decode_ep": 1, + "decode_num_workers": 2, + "eval_concs": [4, 16], + "completed_eval_concs": [4, 16], + "failed_eval_concs": [], + "conc": 4, + "eval_suite": "gsm8k", + } + ) + ) _write_lm_eval_result( artifact_dir / "results_test_conc4.json", 0.90, @@ -100,13 +113,17 @@ def test_collect_eval_rows_ignores_failed_batch_points( ) -> None: artifact_dir = tmp_path / "eval_batch" artifact_dir.mkdir() - (artifact_dir / "meta_env.json").write_text(json.dumps({ - "is_multinode": True, - "eval_concs": [4, 16], - "completed_eval_concs": [4], - "failed_eval_concs": [16], - "conc": 4, - })) + (artifact_dir / "meta_env.json").write_text( + json.dumps( + { + "is_multinode": True, + "eval_concs": [4, 16], + "completed_eval_concs": [4], + "failed_eval_concs": [16], + "conc": 4, + } + ) + ) _write_lm_eval_result( artifact_dir / "results_test_conc4.json", 0.90, @@ -121,7 +138,6 @@ def test_collect_eval_rows_ignores_failed_batch_points( assert [row["conc"] for row in rows] == [4] - def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None: artifact_dir = tmp_path / "eval_provider" artifact_dir.mkdir() @@ -139,4 +155,4 @@ def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None assert len(rows) == 1 assert rows[0]["score"] == 1.0 - assert rows[0]["eval_suite"] == "provider_smoke" \ No newline at end of file + assert rows[0]["eval_suite"] == "provider_smoke" From 6ec09bbd883a47688240fb6f77ed99beee27c41c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:14:50 -0500 Subject: [PATCH 09/99] chore: preserve existing collector test formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留结果收集器测试的现有格式,仅添加格式契约测试。 --- utils/test_collect_eval_results.py | 105 +++++++++++++---------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 2c3b54a02a..fa7f9901dc 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -39,27 +39,23 @@ def test_build_row_preserves_explicit_eval_suite() -> None: def _write_lm_eval_result(path: Path, score: float) -> None: - path.write_text( - json.dumps( - { - "lm_eval_version": "0.4.0", - "model_name": "test-model", - "results": { - "gsm8k": { - "exact_match,strict-match": score, - "exact_match_stderr,strict-match": 0.01, - }, - }, - "configs": { - "gsm8k": { - "metric_list": [{"metric": "exact_match"}], - "filter_list": [{"name": "strict-match"}], - }, - }, - "n-samples": {"gsm8k": {"effective": 10}}, - } - ) - ) + path.write_text(json.dumps({ + "lm_eval_version": "0.4.0", + "model_name": "test-model", + "results": { + "gsm8k": { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.01, + }, + }, + "configs": { + "gsm8k": { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], + }, + }, + "n-samples": {"gsm8k": {"effective": 10}}, + })) def test_collect_eval_rows_expands_batched_concurrencies( @@ -67,31 +63,27 @@ def test_collect_eval_rows_expands_batched_concurrencies( ) -> None: artifact_dir = tmp_path / "eval_batch" artifact_dir.mkdir() - (artifact_dir / "meta_env.json").write_text( - json.dumps( - { - "is_multinode": True, - "infmax_model_prefix": "gptoss", - "hw": "gb200", - "framework": "dynamo-sglang", - "precision": "fp8", - "spec_decoding": "none", - "isl": 8192, - "osl": 1024, - "prefill_tp": 4, - "prefill_ep": 1, - "prefill_num_workers": 1, - "decode_tp": 8, - "decode_ep": 1, - "decode_num_workers": 2, - "eval_concs": [4, 16], - "completed_eval_concs": [4, 16], - "failed_eval_concs": [], - "conc": 4, - "eval_suite": "gsm8k", - } - ) - ) + (artifact_dir / "meta_env.json").write_text(json.dumps({ + "is_multinode": True, + "infmax_model_prefix": "gptoss", + "hw": "gb200", + "framework": "dynamo-sglang", + "precision": "fp8", + "spec_decoding": "none", + "isl": 8192, + "osl": 1024, + "prefill_tp": 4, + "prefill_ep": 1, + "prefill_num_workers": 1, + "decode_tp": 8, + "decode_ep": 1, + "decode_num_workers": 2, + "eval_concs": [4, 16], + "completed_eval_concs": [4, 16], + "failed_eval_concs": [], + "conc": 4, + "eval_suite": "gsm8k", + })) _write_lm_eval_result( artifact_dir / "results_test_conc4.json", 0.90, @@ -113,17 +105,13 @@ def test_collect_eval_rows_ignores_failed_batch_points( ) -> None: artifact_dir = tmp_path / "eval_batch" artifact_dir.mkdir() - (artifact_dir / "meta_env.json").write_text( - json.dumps( - { - "is_multinode": True, - "eval_concs": [4, 16], - "completed_eval_concs": [4], - "failed_eval_concs": [16], - "conc": 4, - } - ) - ) + (artifact_dir / "meta_env.json").write_text(json.dumps({ + "is_multinode": True, + "eval_concs": [4, 16], + "completed_eval_concs": [4], + "failed_eval_concs": [16], + "conc": 4, + })) _write_lm_eval_result( artifact_dir / "results_test_conc4.json", 0.90, @@ -138,6 +126,7 @@ def test_collect_eval_rows_ignores_failed_batch_points( assert [row["conc"] for row in rows] == [4] + def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None: artifact_dir = tmp_path / "eval_provider" artifact_dir.mkdir() @@ -155,4 +144,4 @@ def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None assert len(rows) == 1 assert rows[0]["score"] == 1.0 - assert rows[0]["eval_suite"] == "provider_smoke" + assert rows[0]["eval_suite"] == "provider_smoke" \ No newline at end of file From 2b6e5e8987abe847472e4e932ed7aeeb91260c84 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:21:37 -0500 Subject: [PATCH 10/99] fix: preserve intended verifier sample count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在启动失败时保留验证器计划样本数,同时将有效样本数记录为零。 --- utils/evals/kimi_vendor_eval.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index f29c621c6a..4fe93fd973 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -133,7 +133,12 @@ def _compatibility_result( "filter_list": [{"name": "strict-match"}], } }, - "n-samples": {TASK_NAME: {"original": n_samples, "effective": n_samples}}, + "n-samples": { + TASK_NAME: { + "original": len(EXPECTED_MODES), + "effective": n_samples, + } + }, } if integration_error is not None: result["integration_error"] = { From 6793c9e3f2bdef98040e9d106b948ab2bf564e06 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:59:22 -0500 Subject: [PATCH 11/99] fix: preserve configurable eval dispatch behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留可配置的 eval 调度行为,并修正失败返回码、Bash 兼容性和 eval 文档。 --- benchmarks/benchmark_lib.sh | 6 +- .../agentic/minimaxm3_fp4_b200_mtp.sh | 2 - .../agentic/minimaxm3_fp4_b300_mtp.sh | 2 - .../agentic/minimaxm3_fp4_mi355x.sh | 7 -- .../agentic/qwen3.5_fp4_b200_sglang_mtp.sh | 3 - .../agentic/qwen3.5_fp4_b300_sglang_mtp.sh | 3 - .../agentic/qwen3.5_fp8_b200_sglang_mtp.sh | 3 - .../agentic/qwen3.5_fp8_b300_sglang_mtp.sh | 3 - docs/eval-agentx-procedures.md | 27 ++++---- docs/eval-agentx-procedures_zh.md | 27 ++++---- utils/evals/EVALS.md | 27 ++++---- utils/evals/test_run_eval_dispatch.py | 68 +++++++++++++++++++ utils/matrix_logic/generate_sweep_configs.py | 20 +++--- 13 files changed, 120 insertions(+), 78 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index a6eb95bd60..6c198ea230 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1240,8 +1240,8 @@ _eval_concs_to_json() { } _env_is_true() { - case "${1,,}" in - 1|true|yes|on) return 0 ;; + case "${1:-}" in + 1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|[Oo][Nn]) return 0 ;; *) return 1 ;; esac } @@ -1899,7 +1899,7 @@ run_eval() { if [ "$eval_rc" -ne 0 ]; then echo "ERROR: run_eval failed with exit code $eval_rc" >&2 - if [ "${EVAL_ONLY}" = "true" ]; then + if [ "${EVAL_ONLY:-false}" = "true" ]; then echo "Eval-only mode: failing after artifact collection" >&2 return "$eval_rc" fi diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh index be705c0c21..58566be51f 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh @@ -21,8 +21,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" - check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION DRAFT_MODEL="Inferact/MiniMax-M3-EAGLE3-GQA" diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh index 3593cac4ce..1b65687c18 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh @@ -7,8 +7,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" - check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION DRAFT_MODEL="Inferact/MiniMax-M3-EAGLE3-GQA" diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x.sh index 7faaf69a51..a82924b167 100644 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x.sh @@ -10,13 +10,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -# Force the eval framework to lm-eval for this recipe. run_eval derives its -# default as swebench for agentic scenarios (scenario_default=swebench when -# IS_AGENTIC/SCENARIO_TYPE=agentic-coding), but EVAL_FRAMEWORK takes precedence -# over that default (benchmark_lib.sh: framework=${EVAL_FRAMEWORK:-...}), so -# setting it here makes the effective framework always lm-eval, never swebench. -export EVAL_FRAMEWORK="lm-eval" - check_env_vars MODEL TP CONC KV_OFFLOADING KV_OFFLOAD_BACKEND TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION echo "MODEL=$MODEL TP=$TP CONC=$CONC KV_OFFLOADING=$KV_OFFLOADING TOTAL_CPU_DRAM_GB=$TOTAL_CPU_DRAM_GB RESULT_DIR=$RESULT_DIR DURATION=$DURATION EP_SIZE=$EP_SIZE DP_ATTENTION=$DP_ATTENTION" diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh index f0fc1d09da..fc956e14a0 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh @@ -8,9 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. -export EVAL_FRAMEWORK="lm-eval" - check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh index a6a0ccf3bd..b43eb06454 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh @@ -8,9 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. -export EVAL_FRAMEWORK="lm-eval" - check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh index 472b9e4b12..536602459b 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh @@ -8,9 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. -export EVAL_FRAMEWORK="lm-eval" - check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh index 2d0a390250..96583ebe82 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh @@ -8,9 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. -export EVAL_FRAMEWORK="lm-eval" - check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/docs/eval-agentx-procedures.md b/docs/eval-agentx-procedures.md index e5c349fd9c..66d96838cb 100644 --- a/docs/eval-agentx-procedures.md +++ b/docs/eval-agentx-procedures.md @@ -18,11 +18,11 @@ There are two distinct layers: the matrix generator decides **which jobs exist** | Normal sweep | no eval option | Throughput jobs plus the selected 8k/1k eval subset | | Throughput only | `--no-evals` | No eval jobs | | Selected eval subset only | `--evals-only` | Jobs have `RUN_EVAL=true`, `EVAL_ONLY=true` | -| Every eligible eval only | `--all-evals` | Equivalent to `--evals-only --all-evals` and includes all fixed-sequence 8k/1k rows, plus single-node agentic SWE-bench rows | +| Every eligible eval only | `--all-evals` | Equivalent to `--evals-only --all-evals` and includes all fixed-sequence 8k/1k rows plus single-node and multi-node agentic GSM8K rows | | Throughput then eval in one recipe | `RUN_EVAL=true`, `EVAL_ONLY=false` | Server starts, throughput runs, then `run_eval` runs | | Eval against a freshly started server | `RUN_EVAL=true`, `EVAL_ONLY=true` | Launcher expands eval context, skips throughput, and runs the eval | -Default selection is scenario-aware. Single-node fixed-sequence evals use the median and highest eligible concurrency for each 8k/1k model/runner/framework/precision/parallelism group. Multi-node evals use the highest eligible concurrency per topology. Concurrency below 16 is not selected. Agentic evals are opt-in, and multi-node agentic eval is unsupported. See [`mark_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L238-L339) and [`mark_all_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L342-L398). +Default selection is scenario-aware. Single-node fixed-sequence evals use the median and highest eligible concurrency for each 8k/1k model/runner/framework/precision/parallelism group. Multi-node evals use the highest eligible concurrency per topology. Concurrency below 16 is not selected. Agentic evals are opt-in; single-node and multi-node agentic rows select their highest eligible concurrency. See [`mark_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L276-L396) and [`mark_all_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L399-L482). On a PR, combine one primary sweep label (normally `full-sweep-fail-fast`) with eval modifiers. `all-evals` expands coverage without suppressing throughput. `evals-only` suppresses throughput. Together they run all eligible evals only. Runs with `evals-only` are not reusable, while normal full sweeps and `all-evals` full sweeps are reusable. Adding or removing a modifier restarts the active sweep ([label policy](../.github/workflows/README.md#pr-eval-modifiers)). @@ -48,14 +48,14 @@ uv run --no-project --with pydantic --with pyyaml --python 3.12 \ --config-files configs/nvidia-master.yaml | jq . ``` -A correct AgentX eval row contains `"scenario-type": "agentic-coding"`, `"run-eval": true`, and `"eval-only": true`. The workflow splits generated rows into throughput, fixed-sequence eval, and agentic eval jobs in [`.github/workflows/e2e-tests.yml`](../.github/workflows/e2e-tests.yml#L257-L271). +A correct AgentX eval row contains `"scenario-type": "agentic-coding"`, `"run-eval": true`, and `"eval-only": true`. The workflow splits generated rows into throughput, fixed-sequence eval, and agentic eval jobs in [`.github/workflows/e2e-tests.yml`](../.github/workflows/e2e-tests.yml#L278-L293). ## 2. Add a graded eval 1. Add `utils/evals/.yaml` using the lm-evaluation-harness task format. Pin the dataset/split, deterministic generation settings, prompt contract, filters, and primary metric. Use [`gsm8k.yaml`](../utils/evals/gsm8k.yaml) or [`gpqa_diamond.yaml`](../utils/evals/gpqa_diamond.yaml) as an in-tree pattern. 2. Give `task:` a stable name. That exact name is the key used by score thresholds and appears in collected rows. 3. Add the minimum accepted score to [`utils/evals/thresholds.yaml`](../utils/evals/thresholds.yaml). Put a general floor under `default`. Add `models..` only when a justified model-specific floor is required. -4. If the task's primary result is not compatible with the collector's strict/extract/accuracy rules, extend [`extract_lm_metrics()`](../utils/collect_eval_results.py#L114-L181). Do not publish a row whose `score` is null. +4. If the task's primary result is not compatible with the collector's strict/extract/accuracy rules, extend [`extract_lm_metrics()`](../utils/collect_eval_results.py#L115-L197). Do not publish a row whose `score` is null. 5. Run a small explicit slice, inspect samples, then run the full split. `EVAL_LIMIT` is a smoke-test control, not a publishable score setting. Against an already healthy OpenAI-compatible server: @@ -97,9 +97,9 @@ Set `EVAL_ONLY=true` **before server launch**. It is not merely a switch inside 4. Throughput returns immediately or is skipped. 5. `run_eval` and artifact staging run. -Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L1049-L1078), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1737-L1856), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L79-L97). +Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L1049-L1078), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1789-L1908), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L79-L97). -Do not toggle `EVAL_ONLY` after a throughput-sized server is already running and assume the context changed. Restart through the recipe. In eval-only mode an eval failure is returned after available artifacts are staged. In a workflow, upload happens with `always()` before score validation so failed evidence survives ([single-node upload and gate](../.github/workflows/benchmark-tmpl.yml#L399-L417), [multi-node upload and gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468)). +Do not toggle `EVAL_ONLY` after a throughput-sized server is already running and assume the context changed. Restart through the recipe. In eval-only mode an eval failure is returned after available artifacts are staged. In a workflow, upload happens with `always()` before score validation so failed evidence survives ([single-node upload and gate](../.github/workflows/benchmark-tmpl.yml#L399-L417), [multi-node upload and gate](../.github/workflows/benchmark-multinode-tmpl.yml#L466-L488)). ## 4. Batched eval concurrency @@ -121,9 +121,9 @@ The batch runner creates a fresh temporary output directory per point, stages fi - `completed_eval_concs`: eval and staging both succeeded. - `failed_eval_concs`: either eval or staging failed. -A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1737-L1832), [artifact suffixing](../benchmarks/benchmark_lib.sh#L1163-L1222), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). +A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1824-L1885), [artifact suffixing](../benchmarks/benchmark_lib.sh#L1163-L1222), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). -For multi-node `all-evals`, the workflow constructs `EVAL_CONC` by joining the topology's concurrency list ([dispatch](../.github/workflows/e2e-tests.yml#L394-L398)). Never compare a point if its `_conc` result or completed-manifest entry is missing. +For multi-node `all-evals`, the workflow constructs `EVAL_CONC` by joining the topology's concurrency list ([dispatch](../.github/workflows/e2e-tests.yml#L397-L400)). Never compare a point if its `_conc` result or completed-manifest entry is missing. ## 5. Validate scores, not file existence @@ -173,7 +173,7 @@ Retain `meta_env.json`, `results*.json`, and `sample*.jsonl`. Agentic SWE-bench ## 7. Run AgentX: fast feedback versus canonical evidence -AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L2026-L2050)). +AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L2089-L2113)). Targeted canonical run (configured duration and warmup, with fast and duration overrides omitted): @@ -199,17 +199,18 @@ Targeted AgentX SWE-bench smoke eval (first ten instances, real agentic generati gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ -f generate-cli-command='test-config --config-keys qwen3.5-fp8-b200-sglang-agentic --conc 1 --evals-only --config-files configs/nvidia-master.yaml' \ -f test-name='swebench-smoke-qwen35-c1' \ + -f eval-framework=swebench \ -f eval-limit='10' \ -f swebench-gen-mode='agentic' ``` For a publishable SWE-bench score, omit `eval-limit`. Do not use `single-shot`, which is only a debugging escape hatch. SWE-bench generation/scoring controls and its `0.50` full-split threshold are documented next to the implementation in [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench). -Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L2188-L2190)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. +Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L2251-L2253)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. ## 8. Preserve trace and run provenance -AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L1945-L2024), [replay semantics](../benchmarks/benchmark_lib.sh#L2026-L2192)). +AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L2008-L2087), [replay semantics](../benchmarks/benchmark_lib.sh#L2089-L2255)). Capture orchestration provenance immediately: @@ -241,7 +242,7 @@ For each concurrency retain: - server/frontend logs and every metrics endpoint represented. - run URL/ID, attempt, head SHA, recipe/config identity, image, topology, fast flag, and any override. -The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2242-L2282)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L349-L358), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448)). +The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2305-L2345)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L349-L358), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464)). ## 9. Debug long AgentX runs from live evidence @@ -290,7 +291,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L2158-L2181)). +Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L2221-L2245)). Use phase markers, not total Slurm age: diff --git a/docs/eval-agentx-procedures_zh.md b/docs/eval-agentx-procedures_zh.md index 904cc3d1b7..c4c8733e69 100644 --- a/docs/eval-agentx-procedures_zh.md +++ b/docs/eval-agentx-procedures_zh.md @@ -18,11 +18,11 @@ | 常规 sweep | 不加 eval 选项 | 吞吐量作业,加上选定的 8k/1k eval 子集 | | 仅吞吐量 | `--no-evals` | 不生成 eval 作业 | | 仅选定的 eval 子集 | `--evals-only` | 作业带有 `RUN_EVAL=true`、`EVAL_ONLY=true` | -| 仅运行所有符合条件的 eval | `--all-evals` | 等价于 `--evals-only --all-evals`;包含全部定长序列 8k/1k 行,以及单节点 agentic SWE-bench 行 | +| 仅运行所有符合条件的 eval | `--all-evals` | 等价于 `--evals-only --all-evals`;包含全部定长序列 8k/1k 行,以及单节点和多节点 agentic GSM8K 行 | | 在一个 recipe 中先跑吞吐量再跑 eval | `RUN_EVAL=true`、`EVAL_ONLY=false` | 启动服务,运行吞吐量,然后执行 `run_eval` | | 对新启动的服务仅运行 eval | `RUN_EVAL=true`、`EVAL_ONLY=true` | launcher 扩大 eval context,跳过吞吐量并运行 eval | -默认选择会区分场景。单节点定长序列 eval 对每个 8k/1k 的模型/runner/framework/precision/并行配置分组选取符合条件的中位和最高并发;多节点 eval 对每种拓扑选取符合条件的最高并发。低于 16 的并发不会被选中。Agentic eval 需要显式启用;不支持多节点 agentic eval。参见 [`mark_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L238-L339) 与 [`mark_all_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L342-L398)。 +默认选择会区分场景。单节点定长序列 eval 对每个 8k/1k 的模型/runner/framework/precision/并行配置分组选取符合条件的中位和最高并发;多节点 eval 对每种拓扑选取符合条件的最高并发。低于 16 的并发不会被选中。Agentic eval 需要显式启用;单节点和多节点 agentic 行均选择符合条件的最高并发。参见 [`mark_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L276-L396) 与 [`mark_all_eval_entries()`](../utils/matrix_logic/generate_sweep_configs.py#L399-L482)。 在 PR 上,应将一个主要 sweep label(通常为 `full-sweep-fail-fast`)与 eval modifier 组合使用。`all-evals` 在不抑制吞吐量的情况下扩大覆盖范围;`evals-only` 会抑制吞吐量;两者一起使用时只运行所有符合条件的 eval。带有 `evals-only` 的运行不可复用,而常规 full sweep 和 `all-evals` full sweep 可以复用。添加或移除 modifier 会重启当前 sweep([label 策略](../.github/workflows/README.md#pr-eval-modifiers))。 @@ -48,14 +48,14 @@ uv run --no-project --with pydantic --with pyyaml --python 3.12 \ --config-files configs/nvidia-master.yaml | jq . ``` -正确的 AgentX eval 行包含 `"scenario-type": "agentic-coding"`、`"run-eval": true` 和 `"eval-only": true`。工作流会在 [`.github/workflows/e2e-tests.yml`](../.github/workflows/e2e-tests.yml#L257-L271) 中将生成的行拆分到吞吐量、定长序列 eval 和 agentic eval 作业。 +正确的 AgentX eval 行包含 `"scenario-type": "agentic-coding"`、`"run-eval": true` 和 `"eval-only": true`。工作流会在 [`.github/workflows/e2e-tests.yml`](../.github/workflows/e2e-tests.yml#L278-L293) 中将生成的行拆分到吞吐量、定长序列 eval 和 agentic eval 作业。 ## 2. 添加评分 eval 1. 按照 lm-evaluation-harness task 格式添加 `utils/evals/.yaml`。固定 dataset/split、确定性生成设置、prompt 约定、filter 和主指标。可参考仓库内的 [`gsm8k.yaml`](../utils/evals/gsm8k.yaml) 或 [`gpqa_diamond.yaml`](../utils/evals/gpqa_diamond.yaml)。 2. 为 `task:` 指定稳定名称。分数阈值以该精确名称为键,收集后的行中也会出现该名称。 3. 在 [`utils/evals/thresholds.yaml`](../utils/evals/thresholds.yaml) 中添加最低可接受分数。通用下限放在 `default`;只有在确有依据需要模型专用下限时,才添加 `models..`。 -4. 如果 task 的主结果与 collector 的 strict/extract/accuracy 规则不兼容,请扩展 [`extract_lm_metrics()`](../utils/collect_eval_results.py#L114-L181)。不要发布 `score` 为 null 的行。 +4. 如果 task 的主结果与 collector 的 strict/extract/accuracy 规则不兼容,请扩展 [`extract_lm_metrics()`](../utils/collect_eval_results.py#L115-L197)。不要发布 `score` 为 null 的行。 5. 先运行一个显式的小切片并检查样本,再运行完整 split。`EVAL_LIMIT` 是 smoke test 控制项,不是可发布分数的运行设置。 对已经健康的 OpenAI-compatible 服务执行: @@ -97,9 +97,9 @@ python3 utils/evals/validate_scores.py --model-prefix "$MODEL_PREFIX" 4. 吞吐量路径立即返回或被跳过。 5. 运行 `run_eval` 和 artifact staging。 -相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L1049-L1078)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1737-L1856) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L79-L97)。 +相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L1049-L1078)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1789-L1908) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L79-L97)。 -不要在吞吐量规格的服务已经运行后才切换 `EVAL_ONLY`,并假定 context 会随之变化。应通过 recipe 重启。Eval-only 模式会在暂存已有 artifact 后返回 eval 失败;在工作流中,上传步骤使用 `always()`,并位于分数校验前,因此失败证据仍会保留([单节点上传与 gate](../.github/workflows/benchmark-tmpl.yml#L399-L417)、[多节点上传与 gate](../.github/workflows/benchmark-multinode-tmpl.yml#L450-L468))。 +不要在吞吐量规格的服务已经运行后才切换 `EVAL_ONLY`,并假定 context 会随之变化。应通过 recipe 重启。Eval-only 模式会在暂存已有 artifact 后返回 eval 失败;在工作流中,上传步骤使用 `always()`,并位于分数校验前,因此失败证据仍会保留([单节点上传与 gate](../.github/workflows/benchmark-tmpl.yml#L399-L417)、[多节点上传与 gate](../.github/workflows/benchmark-multinode-tmpl.yml#L466-L488))。 ## 4. 批量 eval 并发 @@ -121,9 +121,9 @@ python3 utils/evals/validate_scores.py --expected-concs '16 32 64' - `completed_eval_concs`:eval 与 staging 均成功的点; - `failed_eval_concs`:eval 或 staging 失败的点。 -失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1737-L1832)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L1163-L1222) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 +失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1824-L1885)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L1163-L1222) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 -对于多节点 `all-evals`,工作流通过连接拓扑的并发列表构造 `EVAL_CONC`([分派](../.github/workflows/e2e-tests.yml#L394-L398))。如果缺少某点的 `_conc` 结果或 completed manifest 条目,绝不能比较该点。 +对于多节点 `all-evals`,工作流通过连接拓扑的并发列表构造 `EVAL_CONC`([分派](../.github/workflows/e2e-tests.yml#L397-L400))。如果缺少某点的 `_conc` 结果或 completed manifest 条目,绝不能比较该点。 ## 5. 校验分数,而不只是检查文件存在 @@ -173,7 +173,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ ## 7. 运行 AgentX:快速反馈与 canonical 证据 -AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[fast replay 设置](../benchmarks/benchmark_lib.sh#L2026-L2050))。 +AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[fast replay 设置](../benchmarks/benchmark_lib.sh#L2089-L2113))。 目标 canonical 运行(使用配置的 duration 和 warmup;不要加 fast 或 duration override): @@ -199,17 +199,18 @@ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ -f generate-cli-command='test-config --config-keys qwen3.5-fp8-b200-sglang-agentic --conc 1 --evals-only --config-files configs/nvidia-master.yaml' \ -f test-name='swebench-smoke-qwen35-c1' \ + -f eval-framework=swebench \ -f eval-limit='10' \ -f swebench-gen-mode='agentic' ``` 要得到可发布的 SWE-bench 分数,省略 `eval-limit`;不要使用 `single-shot`,它只是诊断逃生选项。SWE-bench generation/scoring 控制项以及完整 split 的 `0.50` 阈值在实现旁的 [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench) 中说明。 -Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L2188-L2190))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 +Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L2251-L2253))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 ## 8. 保留 trace 与运行 provenance -AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L1945-L2024)、[replay 语义](../benchmarks/benchmark_lib.sh#L2026-L2192))。 +AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L2008-L2087)、[replay 语义](../benchmarks/benchmark_lib.sh#L2089-L2255))。 立即记录 orchestration provenance: @@ -241,7 +242,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ - server/frontend 日志以及所代表的每个 metrics endpoint; - run URL/ID、attempt、head SHA、recipe/config 标识、image、topology、fast 标志和所有 override。 -Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2242-L2282))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L349-L358)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L439-L448))。 +Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2305-L2345))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L349-L358)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464))。 ## 9. 用实时证据调试长时间 AgentX 运行 @@ -290,7 +291,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L2158-L2181))。 +通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L2221-L2245))。 应使用 phase marker,而不是 Slurm 总运行时间: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index ce3e6a07b6..f808ce9d88 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -11,11 +11,10 @@ from throughput. Selection lives in `mark_eval_entries()` in runner, framework, precision, TP, and decoding configuration. - **Multi-node:** 8k1k only, with one job per parallelism topology at its highest eligible concurrency. Rows differing only by concurrency share a topology. -- **Agentic (SWE-bench), single-node:** highest-conc entry per (model, - runner, framework, precision) group. -- **Agentic (SWE-bench), multi-node:** same policy as multi-node fixed-seq-len - above (highest eligible conc per parallelism topology), since SWE-bench - doesn't support batched concurrencies the way lm-eval does. +- **Agentic (GSM8K), single-node:** highest-conc entry per (model, runner, + framework, precision) group. +- **Agentic (GSM8K), multi-node:** highest eligible concurrency per + parallelism topology. Generator eval modes: @@ -128,14 +127,14 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | -| `_prepare_kimi_vendor_runtime` | Installs the minimal pinned runtime in an isolated temp path | +| `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | | `_prepare_kimi_vendor_verifier` | Fetches a fresh pinned sparse checkout | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | ### Single-node -In eval-only mode (`EVAL_ONLY=true`), the benchmark script computes `EVAL_MAX_MODEL_LEN` via `compute_eval_context_length`, starts the server with that context length, skips throughput, and runs lm-eval directly. Each framework wires that context differently (`--context-length` for SGLang, `--max_seq_len` for TRT-LLM). +For default lm-eval jobs in eval-only mode (`EVAL_ONLY=true`), the benchmark script computes `EVAL_MAX_MODEL_LEN` via `compute_eval_context_length`, starts the server with that context length, skips throughput, and runs lm-eval. Each framework wires that context differently (`--context-length` for SGLang, `--max_seq_len` for TRT-LLM). ### Multi-node Multi-node evals support two hardware paths: @@ -143,13 +142,13 @@ Multi-node evals support two hardware paths: **MI355X (AMD)** — `benchmarks/multi_node/amd_utils/server_sglang.sh` - Skips throughput when `EVAL_ONLY=true` - Fixed-seq-len: runs lm-eval via `run_eval --framework lm-eval` against the router on port 30000 -- Agentic-coding (disaggregated, `IS_AGENTIC=1`): runs SWE-bench via `run_eval --port 30000` (no - `--framework` override, same auto-selection as single-node agentic eval-only). Since there's no - single "TP" for a disaggregated topology, and the workflow spells a couple of metadata fields - differently (`PREFILL_DP_ATTN`/`DECODE_DP_ATTN`) than `append_lm_eval_summary` expects - (`PREFILL_DP_ATTENTION`/`DECODE_DP_ATTENTION`), the agentic branch bridges those before calling - `run_eval`; `append_lm_eval_summary` itself runs automatically inside `run_eval()` (same - `EVAL_ONLY=true && IS_AGENTIC` auto-staging as single-node), not as a separate call. +- Agentic-coding (disaggregated, `IS_AGENTIC=1`): follows the same GSM8K/lm-eval path via + `run_eval --framework lm-eval`. Since there's no single "TP" for a disaggregated topology, + and the workflow spells a couple of metadata fields differently + (`PREFILL_DP_ATTN`/`DECODE_DP_ATTN`) than `append_lm_eval_summary` expects + (`PREFILL_DP_ATTENTION`/`DECODE_DP_ATTENTION`), the agentic branch bridges those before + calling `run_eval`; `append_lm_eval_summary` itself runs automatically inside `run_eval()` + (same `EVAL_ONLY=true && IS_AGENTIC` auto-staging as single-node), not as a separate call. - Concurrency uses workflow-provided `EVAL_CONC` when set, otherwise falls back to max of `BENCH_MAX_CONCURRENCY` (x-separated values) - Eval artifacts copied to `/run_logs/slurm_job-*/eval_results/` - `runners/launch_mi355x-amds.sh` skips benchmark result collection when `EVAL_ONLY=true` and uses `find` to locate eval results diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index cad7cb09ad..de98935c8c 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -118,6 +118,30 @@ def test_kimi_vendor_skips_unused_model_context_loading() -> None: assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout +def test_kimi_failure_preserves_rc_without_eval_only() -> None: + script = r''' +set -u +source "$BENCHMARK_LIB" +run_kimi_vendor_eval() { return 7; } +export EVAL_FRAMEWORK=kimi-vendor +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_MAX_MODEL_LEN=16384 +export IS_AGENTIC=0 +unset EVAL_ONLY +run_eval --port 8888 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 7 + assert "unbound variable" not in result.stderr + + def test_recipe_lm_eval_arg_still_lm_eval_on_fixed_seqlen(): @@ -509,6 +533,50 @@ def test_summary_metadata_prefers_explicit_suite_then_task_basename( assert explicit["eval_suite"] == "kimi_tool_call_schema" +def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: + script = r''' +set -u +source "$BENCHMARK_LIB" +for value in TrUe yEs oN 1 false 0; do + if _env_is_true "$value"; then + echo true + else + echo false + fi +done +for empty_call in with-argument without-argument; do + if [ "$empty_call" = "with-argument" ]; then + _env_is_true "" + else + _env_is_true + fi + if [ "$?" -eq 0 ]; then + echo true + else + echo false + fi +done +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=True, + ) + + assert result.stdout.splitlines() == [ + "true", + "true", + "true", + "true", + "false", + "false", + "false", + "false", + ] + + _MODAL_CREDS_SCRIPT = r''' source "$BENCHMARK_LIB" diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 5b2dee4108..815eefb01a 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -289,10 +289,9 @@ def mark_eval_entries(matrix_values: list[dict], include_agentic: bool = False) - Single-node: run GSM8K through the same lm-eval path as fixed-sequence 8k1k evals, marking the highest-conc entry per (model, runner, framework, precision) group. - - Multi-node: same policy as the fixed-seq-len multi-node case above - (highest eligible conc per distinct parallelism config, via - eval-conc), using SWE-bench since it doesn't support batched - concurrencies. + - Multi-node: run GSM8K through the same lm-eval path, selecting the + highest eligible concurrency per distinct parallelism config via + eval-conc. """ from collections import defaultdict @@ -360,8 +359,7 @@ def _eligible_eval_concs(entry): ag_sn_groups = defaultdict(list) # Multi-node agentic: same "highest eligible conc per distinct # parallelism config" policy as the fixed-seq-len mn_groups above. - # SWE-bench doesn't support batched concurrencies (unlike lm-eval), - # so exactly one conc is picked per group, never the full list. + # The selected eval subset uses exactly one conc per group. ag_mn_groups = defaultdict(list) for i, entry in enumerate(matrix_values): if entry.get(Fields.SCENARIO_TYPE.value) != 'agentic-coding': @@ -402,12 +400,10 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: Evals only run at 8k1k (matching mark_eval_entries), so entries at other sequence lengths (e.g. 1k1k) are passed through untouched rather than expanded into eval rows. - Single-node agentic entries use GSM8K through the same lm-eval path as - fixed-sequence 8k1k evals. Multi-node agentic entries use SWE-bench, - which doesn't support batched concurrencies (unlike lm-eval): multi-node - agentic rows with the same topology are merged (to recombine any chunking - split), but only the highest resulting conc is marked for eval via - eval-conc, not the full list. + Single- and multi-node agentic entries use GSM8K through lm-eval. + Multi-node agentic rows with the same topology are merged (to recombine + any chunking split), but only the highest resulting conc is marked for + eval via eval-conc, matching the default agentic selection policy. Multi-node fixed-seq-len rows with the same engine topology are merged into one eval row whose full concurrency list is run sequentially against the same engine. From 294e39d51753ca27c9ee99d310a96e8614a68ffb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:37:45 -0500 Subject: [PATCH 12/99] fix: harden verifier review paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:强化验证器评审路径。 --- .github/workflows/benchmark-tmpl.yml | 2 +- .github/workflows/e2e-tests.yml | 10 ++-- .github/workflows/run-sweep.yml | 12 ++--- benchmarks/benchmark_lib.sh | 13 +++++ .../agentic/glm5.2_fp4_mi355x_sglang_mtp.sh | 1 - utils/collect_eval_results.py | 4 +- utils/evals/EVALS.md | 13 +++-- utils/evals/kimi_vendor_eval.py | 27 +++++++---- utils/evals/test_kimi_vendor_eval.py | 8 ++++ utils/evals/test_run_eval_dispatch.py | 35 ++++++++++++++ utils/test_collect_eval_results.py | 48 ++++++++++++++++++- 11 files changed, 142 insertions(+), 31 deletions(-) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index afafe2585f..68aeaf7a4a 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -91,7 +91,7 @@ on: required: false default: "lm-eval" eval-suite: - description: "Suite interpreted by the selected eval runner" + description: "Kimi Vendor Verifier suite; leave empty for other eval runners" type: string required: false default: "" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 1b0aa95cbb..7b483a57b1 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,12 +46,12 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Single-node agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Agentic eval suite interpreted by the selected runner" + description: "Single-node Kimi Vendor Verifier suite; empty for other runners" required: false type: string default: "" @@ -136,12 +136,12 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Single-node agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Agentic eval suite interpreted by the selected runner" + description: "Single-node Kimi Vendor Verifier suite; empty for other runners" required: false type: string default: "" @@ -256,7 +256,7 @@ jobs: CMD+=(--evals-only) fi RAW_CONFIG_JSON=$("${CMD[@]}") - CONFIG_JSON=$(python3 -c 'import json,sys; data=json.load(sys.stdin); rows=[row for family in ("single_node","multi_node") for group in data.get(family,{}).values() for row in group]; rows.extend(row for family in ("evals","agentic_evals","multinode_evals") for row in data.get(family,[])); print(json.dumps(rows))' <<<"$RAW_CONFIG_JSON") + CONFIG_JSON=$(python3 -c 'import json,sys; data=json.load(sys.stdin); rows=[row for family in ("single_node","multi_node") for group in data.get(family,{}).values() for row in group]; rows.extend(row for family in ("evals","agentic_evals","multinode_evals","multinode_agentic_evals") for row in data.get(family,[])); print(json.dumps(rows))' <<<"$RAW_CONFIG_JSON") else GENERATE_COMMAND="${{ inputs.generate-cli-command || github.event.inputs.generate-cli-command }}" if [ -z "$GENERATE_COMMAND" ]; then diff --git a/.github/workflows/run-sweep.yml b/.github/workflows/run-sweep.yml index b8e95f0806..62d013934b 100644 --- a/.github/workflows/run-sweep.yml +++ b/.github/workflows/run-sweep.yml @@ -853,12 +853,12 @@ jobs: eval-only: true eval-conc: ${{ matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} - # Multi-node agentic (SWE-bench) eval rows carry the agentic input shape, - # so they are dispatched with sweep-multi-node-agentic's inputs rather - # than sweep-multi-node-evals' fixed-seq-len inputs (isl/osl/max-model-len, - # which agentic rows don't have). SWE-bench doesn't support batched - # concurrencies (unlike lm-eval), so eval-conc is always a single value, - # never the joined-list form sweep-multi-node-evals uses. + # Multi-node agentic GSM8K eval rows carry the agentic input shape, so + # they are dispatched with sweep-multi-node-agentic's inputs rather than + # sweep-multi-node-evals' fixed-seq-len inputs (isl/osl/max-model-len, + # which agentic rows don't have). Agentic selection uses one highest + # eval-conc per topology; fixed-sequence --all-evals rows may instead pass + # a joined concurrency list to lm-eval. sweep-multi-node-agentic-evals: needs: [setup, canary-select, canary-sweep] if: >- diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 6c198ea230..63edf03fe9 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1815,6 +1815,19 @@ run_eval() { local framework="${EVAL_FRAMEWORK:-${cli_framework:-$scenario_default}}" + case "${EVAL_SUITE:-}" in + "") ;; + *[!A-Za-z0-9_.-]*) + echo "ERROR: EVAL_SUITE may contain only letters, digits, '.', '_', and '-'" >&2 + return 2 + ;; + esac + + if [ -n "${EVAL_SUITE:-}" ] && [ "$framework" != "kimi-vendor" ]; then + echo "ERROR: EVAL_SUITE is only supported with EVAL_FRAMEWORK=kimi-vendor" >&2 + return 2 + fi + # Kimi Vendor Verifier uses a fixed request budget and does not consume # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for that path. if [ "$framework" != "kimi-vendor" ] && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then diff --git a/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh b/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh index d7ed7a359d..62ec5db23a 100644 --- a/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh @@ -4,7 +4,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" - export EVAL_FRAMEWORK="lm-eval" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 1070a9305e..28d9ab11c9 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -408,7 +408,7 @@ def main(): f"{pct(r['score'])}{se(r['score_se'])}", f"{pct(r['em_strict'])}{se(r['em_strict_se'])}", f"{pct(r['em_flexible'])}{se(r['em_flexible_se'])}", - r['n_eff'] or '', + r['n_eff'] if r['n_eff'] is not None else '', r['model'], ] for r in single_node_rows @@ -446,7 +446,7 @@ def main(): f"{pct(r['score'])}{se(r['score_se'])}", f"{pct(r['em_strict'])}{se(r['em_strict_se'])}", f"{pct(r['em_flexible'])}{se(r['em_flexible_se'])}", - r['n_eff'] or '', + r['n_eff'] if r['n_eff'] is not None else '', r['model'], ] for r in multinode_rows diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index f808ce9d88..58237912f0 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -50,11 +50,16 @@ The default eval framework is [lm-evaluation-harness](https://github.com/Eleuthe The Phase 1 Kimi smoke is opt-in and single-node only. Select `eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema` on -`e2e-tests.yml`, or invoke it after a server is ready: +`e2e-tests.yml`, or invoke it from the repository root after a server is ready: ```bash -EVAL_FRAMEWORK=kimi-vendor EVAL_SUITE=kimi_tool_call_schema \ - run_eval --port "$PORT" +source benchmarks/benchmark_lib.sh +export EVAL_FRAMEWORK=kimi-vendor +export EVAL_SUITE=kimi_tool_call_schema +export EVAL_RESULT_DIR="$(mktemp -d /tmp/eval_out-XXXXXX)" +run_eval --port "$PORT" +append_lm_eval_summary +python3 utils/evals/validate_scores.py ``` The framework selects a provider-specific subprocess adapter, while the suite @@ -218,7 +223,7 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `RUN_EVAL` | `false` | Enable eval after throughput benchmark | | `EVAL_ONLY` | `false` | Skip throughput, only run evals (set by workflow) | | `EVAL_FRAMEWORK` | `lm-eval` | Eval runner (`lm-eval`, `swebench`, or `kimi-vendor`) | -| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Runner-specific suite selector and artifact identity; the workflow `eval-suite` input sets it explicitly | +| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Provider suite selector and artifact identity. External override is currently supported only by `kimi-vendor`; other runners derive it from their task | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 4fe93fd973..4f3debc6fb 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -74,16 +74,19 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: total = summary.get("total") by_status = _mapping(summary.get("by_status"), "report.summary.by_status") - passed = by_status.get("passed", 0) - if ( - not isinstance(total, int) - or isinstance(total, bool) - or not isinstance(passed, int) - or isinstance(passed, bool) - or passed < 0 - or passed > 2 - ): + if not isinstance(total, int) or isinstance(total, bool): raise ValueError("report summary contains invalid counts") + for status, count in by_status.items(): + if ( + status not in {"passed", "failed"} + or not isinstance(count, int) + or isinstance(count, bool) + or count < 0 + ): + raise ValueError("report summary contains invalid counts") + if sum(by_status.values()) != total: + raise ValueError("report summary does not match total") + passed = by_status.get("passed", 0) modes: list[str] = [] result_passes = 0 @@ -91,7 +94,11 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: record = _mapping(result, f"report.results[{index}]") mode = record.get("mode") status = record.get("status") - if not isinstance(mode, str) or not isinstance(status, str): + if ( + not isinstance(mode, str) + or not isinstance(status, str) + or status not in {"passed", "failed"} + ): raise ValueError(f"report.results[{index}] has invalid mode or status") modes.append(mode) result_passes += status == "passed" diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index fe8394efc8..e133d3b735 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -25,6 +25,12 @@ def _report(stream_status: str = "passed") -> dict[str, Any]: } +def _report_with_inconsistent_counts() -> dict[str, Any]: + report = _report("failed") + report["summary"]["by_status"]["failed"] = 2 + return report + + def _result(output_dir: Path) -> dict[str, Any]: paths = list(output_dir.glob(kve.COMPATIBILITY_GLOB)) assert len(paths) == 1 @@ -134,6 +140,8 @@ def fake_run( (None, "FileNotFoundError"), ("{bad-json", "JSONDecodeError"), (OSError("boom"), "OSError"), + (json.dumps(_report("skipped")), "ValueError"), + (json.dumps(_report_with_inconsistent_counts()), "ValueError"), ( subprocess.TimeoutExpired("pytest", kve.DEFAULT_TIMEOUT_SECONDS), "TimeoutExpired", diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index de98935c8c..0f20df2934 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -170,6 +170,24 @@ def test_run_eval_rejects_missing_framework_value(): assert "--framework requires a value" in result.stderr +def test_run_eval_rejects_unsafe_suite_name() -> None: + result = _run_invalid_call( + "EVAL_SUITE='kimi\"suite' run_eval --framework kimi-vendor" + ) + + assert result.returncode == 2 + assert "EVAL_SUITE may contain only" in result.stderr + + +def test_run_eval_rejects_suite_override_for_lm_eval() -> None: + result = _run_invalid_call( + "EVAL_SUITE=gpqa_diamond run_eval --framework lm-eval" + ) + + assert result.returncode == 2 + assert "only supported with EVAL_FRAMEWORK=kimi-vendor" in result.stderr + + def test_kimi_vendor_rejects_batched_concurrency() -> None: result = _run_invalid_call( "EVAL_MAX_MODEL_LEN=16384 " @@ -1081,3 +1099,20 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + + + +def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + get_jobs = next( + step + for step in workflow["jobs"]["get-jobs"]["steps"] + if step.get("id") == "get-jobs" + ) + flatten_command = next( + line + for line in get_jobs["run"].splitlines() + if "rows.extend" in line + ) + + assert '"multinode_agentic_evals"' in flatten_command \ No newline at end of file diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index fa7f9901dc..41cd5cf3d0 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -1,9 +1,15 @@ """Tests for eval result aggregation.""" import json +import sys from pathlib import Path -from collect_eval_results import EVAL_RESULT_FORMAT, build_row, collect_eval_rows +from collect_eval_results import ( + EVAL_RESULT_FORMAT, + build_row, + collect_eval_rows, + main as collect_main, +) from evals.kimi_vendor_eval import RESULT_FORMAT as KIMI_VENDOR_RESULT_FORMAT @@ -144,4 +150,42 @@ def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None assert len(rows) == 1 assert rows[0]["score"] == 1.0 - assert rows[0]["eval_suite"] == "provider_smoke" \ No newline at end of file + assert rows[0]["eval_suite"] == "provider_smoke" + + +def test_main_renders_zero_effective_samples( + tmp_path: Path, + monkeypatch, + capsys, +) -> None: + for name, is_multinode in (("single", False), ("multi", True)): + artifact_dir = tmp_path / f"eval_{name}" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text(json.dumps({ + "is_multinode": is_multinode, + "eval_suite": "gsm8k", + })) + result_path = artifact_dir / f"results_{name}.json" + _write_lm_eval_result(result_path, 0.0) + result = json.loads(result_path.read_text()) + result["n-samples"]["gsm8k"]["effective"] = 0 + result_path.write_text(json.dumps(result)) + + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + sys, + "argv", + ["collect_eval_results.py", str(tmp_path), "zero-samples"], + ) + + collect_main() + + task_rows = [ + line + for line in capsys.readouterr().out.splitlines() + if "| gsm8k " in line + ] + assert len(task_rows) == 2 + for row in task_rows: + cells = [cell.strip() for cell in row.split("|")[1:-1]] + assert cells[-2] == "0" \ No newline at end of file From ce37f468c03ad2c4f65c808b544a5f2cd45667f8 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:36:29 -0500 Subject: [PATCH 13/99] fix: scope eval state and links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:限定评估套件状态的作用域,并修正双语 AgentX 文档中的源码行号链接。 --- benchmarks/benchmark_lib.sh | 2 ++ docs/eval-agentx-procedures.md | 14 ++++++------ docs/eval-agentx-procedures_zh.md | 14 ++++++------ utils/evals/test_run_eval_dispatch.py | 33 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 63edf03fe9..0043fb901b 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1789,6 +1789,8 @@ run_swebench_eval() { run_eval() { local cli_framework="" local forwarded=() + # Keep runner-selected suite identity scoped to this invocation. + local EVAL_SUITE="${EVAL_SUITE:-}" while [[ $# -gt 0 ]]; do case "$1" in diff --git a/docs/eval-agentx-procedures.md b/docs/eval-agentx-procedures.md index 66d96838cb..263927d908 100644 --- a/docs/eval-agentx-procedures.md +++ b/docs/eval-agentx-procedures.md @@ -97,7 +97,7 @@ Set `EVAL_ONLY=true` **before server launch**. It is not merely a switch inside 4. Throughput returns immediately or is skipped. 5. `run_eval` and artifact staging run. -Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L1049-L1078), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1789-L1908), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L79-L97). +Relevant implementation: [context setup](../benchmarks/benchmark_lib.sh#L1049-L1078), [eval dispatch and failure policy](../benchmarks/benchmark_lib.sh#L1789-L1923), and [workflow inputs](../.github/workflows/benchmark-tmpl.yml#L79-L97). Do not toggle `EVAL_ONLY` after a throughput-sized server is already running and assume the context changed. Restart through the recipe. In eval-only mode an eval failure is returned after available artifacts are staged. In a workflow, upload happens with `always()` before score validation so failed evidence survives ([single-node upload and gate](../.github/workflows/benchmark-tmpl.yml#L399-L417), [multi-node upload and gate](../.github/workflows/benchmark-multinode-tmpl.yml#L466-L488)). @@ -121,7 +121,7 @@ The batch runner creates a fresh temporary output directory per point, stages fi - `completed_eval_concs`: eval and staging both succeeded. - `failed_eval_concs`: either eval or staging failed. -A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1824-L1885), [artifact suffixing](../benchmarks/benchmark_lib.sh#L1163-L1222), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). +A failed point is deferred so artifacts from every attempted point can upload. The post-upload validator then fails the job. Batched mode accepts positive integers and supports only `lm-eval`. See [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1839-L1900), [artifact suffixing](../benchmarks/benchmark_lib.sh#L1163-L1222), and [manifest validation](../utils/evals/validate_scores.py#L72-L171). For multi-node `all-evals`, the workflow constructs `EVAL_CONC` by joining the topology's concurrency list ([dispatch](../.github/workflows/e2e-tests.yml#L397-L400)). Never compare a point if its `_conc` result or completed-manifest entry is missing. @@ -173,7 +173,7 @@ Retain `meta_env.json`, `results*.json`, and `sample*.jsonl`. Agentic SWE-bench ## 7. Run AgentX: fast feedback versus canonical evidence -AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L2089-L2113)). +AgentX is AIPerf `inferencex-agentx-mvp` trace replay, not a fixed-token synthetic benchmark. The checked-in default uses ten additional warmup requests per trajectory lane and the recipe's configured profile duration. `agentx-fast` forces one warmup request per lane and a 1,200-second profile. It affects single- and multi-node AgentX throughput only. Fixed-sequence throughput and evals remain canonical. Fast runs are not eligible for artifact reuse ([workflow policy](../.github/workflows/README.md#agentx-fast-mode), [fast replay settings](../benchmarks/benchmark_lib.sh#L2104-L2128)). Targeted canonical run (configured duration and warmup, with fast and duration overrides omitted): @@ -206,11 +206,11 @@ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ For a publishable SWE-bench score, omit `eval-limit`. Do not use `single-shot`, which is only a debugging escape hatch. SWE-bench generation/scoring controls and its `0.50` full-split threshold are documented next to the implementation in [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench). -Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L2251-L2253)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. +Treat fast results as bring-up evidence, never as a replacement for the canonical candidate. A duration below 900 seconds or `AIPERF_UNSAFE_OVERRIDE=true` adds AIPerf's `--unsafe-override` and flags the submission invalid. Use it only for smoke diagnosis ([source](../benchmarks/benchmark_lib.sh#L2266-L2268)). After a fast run is healthy, run the exact candidate canonically before claiming benchmark success. ## 8. Preserve trace and run provenance -AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L2008-L2087), [replay semantics](../benchmarks/benchmark_lib.sh#L2089-L2255)). +AgentX defaults to recorded assistant-response replay. Live server outputs are measured but discarded when constructing later turns. Set `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1` only for an explicitly different live-assistant experiment. The selected trace corpus is model-family dependent unless `WEKA_LOADER_OVERRIDE` pins it. The resolver logs both loader and Hugging Face dataset ([trace resolution](../benchmarks/benchmark_lib.sh#L2023-L2102), [replay semantics](../benchmarks/benchmark_lib.sh#L2104-L2270)). Capture orchestration provenance immediately: @@ -242,7 +242,7 @@ For each concurrency retain: - server/frontend logs and every metrics endpoint represented. - run URL/ID, attempt, head SHA, recipe/config identity, image, topology, fast flag, and any override. -The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2305-L2345)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L349-L358), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464)). +The runner writes the command before replay and validates raw results after aggregation ([execution path](../benchmarks/benchmark_lib.sh#L2320-L2360)). Aggregation preserves dataset provenance and hardware/model/topology fields ([aggregate construction](../utils/agentic/aggregation/process_agentic_result.py#L194-L272)). Raw workflow uploads intentionally omit very large `inputs.json` and `profile_export_raw.jsonl`. If those are required for an investigation, preserve them from the live allocation before cleanup ([single-node artifact contract](../.github/workflows/benchmark-tmpl.yml#L349-L358), [multi-node contract](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464)). ## 9. Debug long AgentX runs from live evidence @@ -291,7 +291,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L2221-L2245)). +Track trends over repeated samples: running/waiting requests, KV usage, prefix hits, input/output token rates, completed/cancelled/errored requests, frontend routing balance, and disaggregated KV transfer. AIPerf records endpoint identity for every server series ([metrics wiring](../benchmarks/benchmark_lib.sh#L2236-L2260)). Use phase markers, not total Slurm age: diff --git a/docs/eval-agentx-procedures_zh.md b/docs/eval-agentx-procedures_zh.md index c4c8733e69..66c205782e 100644 --- a/docs/eval-agentx-procedures_zh.md +++ b/docs/eval-agentx-procedures_zh.md @@ -97,7 +97,7 @@ python3 utils/evals/validate_scores.py --model-prefix "$MODEL_PREFIX" 4. 吞吐量路径立即返回或被跳过。 5. 运行 `run_eval` 和 artifact staging。 -相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L1049-L1078)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1789-L1908) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L79-L97)。 +相关实现:[context 设置](../benchmarks/benchmark_lib.sh#L1049-L1078)、[eval 分派与失败策略](../benchmarks/benchmark_lib.sh#L1789-L1923) 和[工作流输入](../.github/workflows/benchmark-tmpl.yml#L79-L97)。 不要在吞吐量规格的服务已经运行后才切换 `EVAL_ONLY`,并假定 context 会随之变化。应通过 recipe 重启。Eval-only 模式会在暂存已有 artifact 后返回 eval 失败;在工作流中,上传步骤使用 `always()`,并位于分数校验前,因此失败证据仍会保留([单节点上传与 gate](../.github/workflows/benchmark-tmpl.yml#L399-L417)、[多节点上传与 gate](../.github/workflows/benchmark-multinode-tmpl.yml#L466-L488))。 @@ -121,7 +121,7 @@ python3 utils/evals/validate_scores.py --expected-concs '16 32 64' - `completed_eval_concs`:eval 与 staging 均成功的点; - `failed_eval_concs`:eval 或 staging 失败的点。 -失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1824-L1885)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L1163-L1222) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 +失败点会延迟报错,使所有已尝试点的 artifact 都能上传;随后 post-upload validator 会使作业失败。批量模式只接受正整数,且仅支持 `lm-eval`。参见 [`run_eval` batching](../benchmarks/benchmark_lib.sh#L1839-L1900)、[artifact 后缀处理](../benchmarks/benchmark_lib.sh#L1163-L1222) 和[manifest 校验](../utils/evals/validate_scores.py#L72-L171)。 对于多节点 `all-evals`,工作流通过连接拓扑的并发列表构造 `EVAL_CONC`([分派](../.github/workflows/e2e-tests.yml#L397-L400))。如果缺少某点的 `_conc` 结果或 completed manifest 条目,绝不能比较该点。 @@ -173,7 +173,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ ## 7. 运行 AgentX:快速反馈与 canonical 证据 -AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[fast replay 设置](../benchmarks/benchmark_lib.sh#L2089-L2113))。 +AgentX 是 AIPerf `inferencex-agentx-mvp` trace replay,不是固定 token 的合成 benchmark。仓库默认设置对每条 trajectory lane 额外执行十个 warmup 请求,并使用 recipe 配置的 profile 时长。`agentx-fast` 强制每条 lane 只运行一个 warmup 请求,并将 profile 设为 1,200 秒。它只影响单节点和多节点 AgentX 吞吐量;定长序列吞吐量与 eval 保持 canonical。Fast 运行不符合 artifact reuse 条件([工作流策略](../.github/workflows/README.md#agentx-fast-mode)、[fast replay 设置](../benchmarks/benchmark_lib.sh#L2104-L2128))。 目标 canonical 运行(使用配置的 duration 和 warmup;不要加 fast 或 duration override): @@ -206,11 +206,11 @@ gh workflow run e2e-tests.yml --repo SemiAnalysisAI/InferenceX --ref "$REF" \ 要得到可发布的 SWE-bench 分数,省略 `eval-limit`;不要使用 `single-shot`,它只是诊断逃生选项。SWE-bench generation/scoring 控制项以及完整 split 的 `0.50` 阈值在实现旁的 [`utils/evals/EVALS.md`](../utils/evals/EVALS.md#swe-bench-lite---framework-swebench) 中说明。 -Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L2251-L2253))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 +Fast 结果只能作为 bring-up 证据,绝不能替代 canonical candidate。小于 900 秒的 duration 或 `AIPERF_UNSAFE_OVERRIDE=true` 会添加 AIPerf 的 `--unsafe-override` 并将 submission 标记为无效;只能用于 smoke 诊断([源码](../benchmarks/benchmark_lib.sh#L2266-L2268))。Fast 运行健康后,必须对完全相同的 candidate 进行 canonical 运行,才能宣称 benchmark 成功。 ## 8. 保留 trace 与运行 provenance -AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L2008-L2087)、[replay 语义](../benchmarks/benchmark_lib.sh#L2089-L2255))。 +AgentX 默认 replay 已记录的 assistant response。实时服务输出会被测量,但构造后续 turn 时会丢弃。只有在明确要进行不同的 live-assistant 实验时,才设置 `AIPERF_DATASET_WEKA_LIVE_ASSISTANT_RESPONSES=1`。除非用 `WEKA_LOADER_OVERRIDE` 固定,否则所选 trace corpus 依赖模型 family;resolver 会同时记录 loader 与 Hugging Face dataset([trace 解析](../benchmarks/benchmark_lib.sh#L2023-L2102)、[replay 语义](../benchmarks/benchmark_lib.sh#L2104-L2270))。 立即记录 orchestration provenance: @@ -242,7 +242,7 @@ gh run download "$RUN_ID" --repo SemiAnalysisAI/InferenceX \ - server/frontend 日志以及所代表的每个 metrics endpoint; - run URL/ID、attempt、head SHA、recipe/config 标识、image、topology、fast 标志和所有 override。 -Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2305-L2345))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L349-L358)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464))。 +Runner 会在 replay 前写入命令,并在聚合后校验原始结果([执行路径](../benchmarks/benchmark_lib.sh#L2320-L2360))。聚合会保留 dataset provenance 以及硬件/模型/拓扑字段([aggregate 构造](../utils/agentic/aggregation/process_agentic_result.py#L194-L272))。工作流的 raw upload 会有意排除体积很大的 `inputs.json` 和 `profile_export_raw.jsonl`;如果调查需要这些文件,应在清理前从实时 allocation 保存([单节点 artifact 约定](../.github/workflows/benchmark-tmpl.yml#L349-L358)、[多节点约定](../.github/workflows/benchmark-multinode-tmpl.yml#L455-L464))。 ## 9. 用实时证据调试长时间 AgentX 运行 @@ -291,7 +291,7 @@ curl -fsS '' | \ rg -i 'request|queue|cache|token|prefill|decode|error|fail' ``` -通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L2221-L2245))。 +通过重复 sample 跟踪趋势:running/waiting request、KV usage、prefix hit、input/output token rate、completed/cancelled/errored request、frontend routing balance,以及 disaggregated KV transfer。AIPerf 会为每条 server series 记录 endpoint identity([metrics 接线](../benchmarks/benchmark_lib.sh#L2236-L2260))。 应使用 phase marker,而不是 Slurm 总运行时间: diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 0f20df2934..fae4aab2de 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -188,6 +188,39 @@ def test_run_eval_rejects_suite_override_for_lm_eval() -> None: assert "only supported with EVAL_FRAMEWORK=kimi-vendor" in result.stderr +def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: + script = r''' +source "$BENCHMARK_LIB" +run_kimi_vendor_eval() { + export EVAL_SUITE=kimi_tool_call_schema + echo "DISPATCH=kimi-vendor SUITE=$EVAL_SUITE" +} +run_lm_eval() { echo "DISPATCH=lm-eval SUITE=${EVAL_SUITE:-unset}"; } +export EVAL_MAX_MODEL_LEN=16384 +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +unset EVAL_SUITE +export EVAL_FRAMEWORK=kimi-vendor +run_eval --port 8888 +export EVAL_FRAMEWORK=lm-eval +run_eval --port 8888 +printf 'FINAL_SUITE=%s\n' "${EVAL_SUITE-unset}" +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "DISPATCH=kimi-vendor SUITE=kimi_tool_call_schema" in result.stdout + assert "DISPATCH=lm-eval SUITE=unset" in result.stdout + assert "FINAL_SUITE=unset" in result.stdout + + def test_kimi_vendor_rejects_batched_concurrency() -> None: result = _run_invalid_call( "EVAL_MAX_MODEL_LEN=16384 " From 405dc0e2186f62f4150014c405d677694576a2fb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:59:54 -0500 Subject: [PATCH 14/99] ci: exclude faulty b300 node from slurm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将发生不可纠正 NVLink 错误的 b300-017 节点排除在 Slurm 分配之外。 --- .github/workflows/benchmark-tmpl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 68aeaf7a4a..27f933dec5 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -209,7 +209,7 @@ env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} # These b300 nodes are currently broken. - SALLOC_EXCLUDE: 'b300-005,b300-006' + SALLOC_EXCLUDE: 'b300-005,b300-006,b300-017' permissions: contents: read From 847982ffefea00377a0b69ad0234663b83bcf397 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:10:04 -0500 Subject: [PATCH 15/99] ci: apply B300 node exclusions to allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:让 B300 启动器将 SALLOC_EXCLUDE 传递给 salloc,避免调度到已知故障节点。 --- runners/launch_b300-nv.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/runners/launch_b300-nv.sh b/runners/launch_b300-nv.sh index cad9ba30aa..5fced02ffb 100644 --- a/runners/launch_b300-nv.sh +++ b/runners/launch_b300-nv.sh @@ -481,8 +481,21 @@ else export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" - SALLOC_TIME_LIMIT="${SALLOC_TIME_LIMIT:-480}" - salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT -N 1 --gres=gpu:$GPU_COUNT --exclusive --mem=0 --time="$SALLOC_TIME_LIMIT" --no-shell --job-name="$RUNNER_NAME" + SALLOC_ARGS=( + --partition="$SLURM_PARTITION" + --account="$SLURM_ACCOUNT" + -N 1 + --gres="gpu:$GPU_COUNT" + --exclusive + --mem=0 + --time="${SALLOC_TIME_LIMIT:-480}" + --no-shell + --job-name="$RUNNER_NAME" + ) + if [[ -n "${SALLOC_EXCLUDE:-}" ]]; then + SALLOC_ARGS+=(--exclude="$SALLOC_EXCLUDE") + fi + salloc "${SALLOC_ARGS[@]}" JOB_ID=$(squeue --name="$RUNNER_NAME" -u "$USER" -h -o %A | head -n1) srun --jobid=$JOB_ID \ From 15b08571445d4e8cf14112a73ed4fe7ab9bc60ca Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:11:38 -0500 Subject: [PATCH 16/99] feat: enable multinode kimi verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:启用多节点 Kimi 验证器 --- .../workflows/benchmark-multinode-tmpl.yml | 14 ++ .github/workflows/e2e-tests.yml | 11 +- benchmarks/benchmark_lib.sh | 10 +- benchmarks/multi_node/agentic_srt.sh | 5 +- .../agentic/agg-b200-tp8pp2-agentic.yaml | 2 +- .../agg-gb200-tep16-balanced-agentic.yaml | 3 +- .../agg-gb200-tp16-latency-agentic.yaml | 3 +- runners/inject_synthetic_acceptance.py | 52 +++-- runners/launch_b200-dgxc.sh | 5 + runners/launch_gb200-nv.sh | 11 +- runners/launch_h200-dgxc-slurm.sh | 5 + runners/patch_srt_eval_dispatch.py | 103 +++++++++ runners/synthetic_injectors/vllm.py | 41 +++- runners/test_slurm_utils.py | 197 ++++++++++++++++++ utils/evals/EVALS.md | 12 +- utils/evals/test_run_eval_dispatch.py | 58 ++++-- 16 files changed, 468 insertions(+), 64 deletions(-) create mode 100755 runners/patch_srt_eval_dispatch.py diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 6b5319c667..675394838a 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -133,6 +133,16 @@ on: type: boolean required: false default: false + eval-framework: + description: "Eval runner (lm-eval, swebench, or kimi-vendor)" + type: string + required: false + default: "lm-eval" + eval-suite: + description: "Kimi Vendor Verifier suite; leave empty for other eval runners" + type: string + required: false + default: "" eval-conc: description: "Concurrency value or space-separated list for eval requests (overrides default max-of-conc-list)" type: string @@ -232,6 +242,8 @@ env: DECODE_HARDWARE: ${{ inputs.decode-hardware }} RUN_EVAL: ${{ inputs.run-eval }} EVAL_ONLY: ${{ inputs.eval-only }} + EVAL_FRAMEWORK: ${{ inputs.eval-framework }} + EVAL_SUITE: ${{ inputs.eval-suite }} EVAL_CONC: ${{ inputs.eval-conc }} EVAL_LIMIT: ${{ inputs.eval-limit }} SWEBENCH_GEN_MODE: ${{ inputs.swebench-gen-mode }} @@ -471,6 +483,7 @@ jobs: path: | meta_env.json results*.json + *_vendor_report.json sample*.jsonl agent_preds.json predictions.jsonl @@ -492,6 +505,7 @@ jobs: run: | rm -f meta_env.json || true rm -f results*.json || true + rm -f *_vendor_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7b483a57b1..a0a31b5e91 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,12 +46,12 @@ on: type: string default: "" eval-framework: - description: "Single-node agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Single-node Kimi Vendor Verifier suite; empty for other runners" + description: "Kimi Vendor Verifier suite; empty for other runners" required: false type: string default: "" @@ -136,12 +136,12 @@ on: type: string default: "" eval-framework: - description: "Single-node agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Single-node Kimi Vendor Verifier suite; empty for other runners" + description: "Kimi Vendor Verifier suite; empty for other runners" required: false type: string default: "" @@ -280,6 +280,7 @@ jobs: MULTI_AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and not x.get('run-eval', False)]))" | score_matrix multi-agentic) MULTI_AGENTIC_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and x.get('run-eval', False)]))" | score_matrix multi-agentic-eval) SINGLE=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and not x.get('eval-only', False)]))" | score_matrix single) + EVALS=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix eval) MULTI=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' in x and x.get('scenario-type') != 'agentic-coding' and not x.get('eval-only', False)]))" | score_matrix multi) MULTI_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix multi-eval) { @@ -598,6 +599,8 @@ jobs: eval-conc: ${{ matrix.config['eval-conc'] }} eval-limit: ${{ inputs.eval-limit }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} + eval-framework: ${{ inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite }} scenario-type: agentic-coding ref: ${{ inputs.ref }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 0043fb901b..7b6454652a 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -929,13 +929,6 @@ _run_kimi_tool_call_schema_eval() { esac done - case "${IS_MULTINODE:-false}" in - true|1) - echo "ERROR: Kimi tool-call schema eval supports single-node only" >&2 - export EVAL_RESULT_DIR="" - return 2 - ;; - esac local model_name="${MODEL_NAME:-${MODEL:-}}" local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/kimi_vendor_eval.py" @@ -1816,6 +1809,9 @@ run_eval() { fi local framework="${EVAL_FRAMEWORK:-${cli_framework:-$scenario_default}}" + if [ "$framework" = "kimi-vendor" ] && [ -z "${EVAL_SUITE:-}" ]; then + EVAL_SUITE="kimi_tool_call_schema" + fi case "${EVAL_SUITE:-}" in "") ;; diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index 79a36da524..dea0881327 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -26,8 +26,6 @@ for concurrency in "${CONCURRENCIES[@]}"; do fi done -resolve_trace_source -install_agentic_deps wait_for_agentic_servers_idle() { local timeout_seconds="${AIPERF_DRAIN_TIMEOUT_SECONDS:-1800}" @@ -98,6 +96,9 @@ raise SystemExit(f"Agentic servers did not drain within {timeout_seconds} second PY } +resolve_trace_source +install_agentic_deps + # The AgentX scenario's first-turn cache-bust marker includes AIPerf's unique # per-invocation benchmark ID. Each point therefore gets a disjoint KV keyspace # while its own warmup and profile phases share markers. This makes sequential diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8pp2-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8pp2-agentic.yaml index a0207d65f2..e6ed84715d 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8pp2-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8pp2-agentic.yaml @@ -80,7 +80,7 @@ backend: TILELANG_CLEANUP_TEMP_FILES: "1" UCX_MEMTYPE_CACHE: "n" UCX_MEMTYPE_REG_WHOLE: "n" - UCX_NET_DEVICES: "mlx5_0:1,mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_10:1,mlx5_11:1" + UCX_NET_DEVICES: "mlx5_0:1,mlx5_1:1,mlx5_2:1,mlx5_3:1,mlx5_4:1,mlx5_5:1,mlx5_6:1,mlx5_7:1" HF_HUB_CACHE: "/hf_hub_cache" HUGGINGFACE_HUB_CACHE: "/hf_hub_cache" vllm_config: diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index bf135c0e33..e22dae3f38 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -101,7 +101,7 @@ backend: max-num-seqs: 32 max-num-batched-tokens: 8192 speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' - compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72,75,78,81,84,87,90,93,96]}' + compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72,75,78,81,84,87,90,93,96],"pass_config":{"fuse_allreduce_rms":false}}' block-size: 64 language-model-only: true disable-custom-all-reduce: true @@ -111,7 +111,6 @@ backend: reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true - compilation-config: '{"pass_config":{"fuse_allreduce_rms":false}}' sbatch_directives: cpus-per-task: "144" diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index baed3f19e7..2566aa62f4 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -98,7 +98,7 @@ backend: max-num-seqs: 8 max-num-batched-tokens: 8192 speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' - compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24]}' + compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24],"pass_config":{"fuse_allreduce_rms":false}}' block-size: 64 language-model-only: true disable-custom-all-reduce: true @@ -108,7 +108,6 @@ backend: reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true - compilation-config: '{"pass_config":{"fuse_allreduce_rms":false}}' sbatch_directives: cpus-per-task: "144" diff --git a/runners/inject_synthetic_acceptance.py b/runners/inject_synthetic_acceptance.py index 80d2740e79..1382454d13 100644 --- a/runners/inject_synthetic_acceptance.py +++ b/runners/inject_synthetic_acceptance.py @@ -1,18 +1,13 @@ #!/usr/bin/env python3 -"""Inject synthetic acceptance parameters into an srt-slurm recipe (generic driver). +"""Configure speculative acceptance in an srt-slurm recipe. -This is the framework-agnostic half of the synthetic-acceptance mechanism. It -decides *whether* to inject (the ``SYNTHETIC_ACCEPTANCE`` flag) and *what* mean -acceptance length to inject, then delegates the actual recipe rewrite to a -framework-specific backend (see ``runners/synthetic_injectors/``). - -The script is a no-op (exit 0, file untouched) when: - - SYNTHETIC_ACCEPTANCE is unset/false, -so existing callers that do not opt in get exactly the previous behavior. When -enabled it requires a backend registered for the given framework; the vLLM -backend is added in a follow-up framework-support change. +Eval-only runs remove synthetic acceptance so generated text is checked against +the target model. Throughput runs inject a configured synthetic acceptance +length only when ``SYNTHETIC_ACCEPTANCE=true``. Framework-specific rewriting +lives under ``runners/synthetic_injectors/``. Environment variables: + EVAL_ONLY "true" to restore real target verification SYNTHETIC_ACCEPTANCE "true" to enable (default: "false") SYNTHETIC_ACCEPTANCE_LENGTH target mean acceptance length; if unset, it is auto-resolved from the reference AL YAML using @@ -72,7 +67,9 @@ def _lookup_al(model_block, num_spec_tokens): if isinstance(model_block, dict): # Thinking matrix form: pick the requested mode, then index by level. if any(str(k).startswith("thinking") for k in model_block): - mode = os.environ.get("THINKING_MODE", "thinking_on").strip() or "thinking_on" + mode = ( + os.environ.get("THINKING_MODE", "thinking_on").strip() or "thinking_on" + ) mode_block = model_block.get(mode) if mode_block is None: sys.exit( @@ -115,7 +112,9 @@ def _resolve_al(config_text, injector, ref_yaml): al = _lookup_al(model_block, num_spec_tokens) if al is None: - sys.exit(f"ERROR: num_spec_tokens={num_spec_tokens} not found for {key} in {ref_yaml}") + sys.exit( + f"ERROR: num_spec_tokens={num_spec_tokens} not found for {key} in {ref_yaml}" + ) _log( f"Auto-resolved AL={al} from {ref_yaml} " @@ -125,14 +124,30 @@ def _resolve_al(config_text, injector, ref_yaml): def inject(config_file, framework): + injector = get_injector(framework) + if _enabled("EVAL_ONLY"): - print("[Synthetic AL] EVAL_ONLY=true: keeping real MTP recipe") + if injector is None or not hasattr(injector, "rewrite_real"): + print( + f"[Synthetic AL] EVAL_ONLY=true: no real-acceptance rewriter " + f"for FRAMEWORK='{framework}'" + ) + return 0 + + with open(config_file) as f: + content = f.read() + new_content, count = injector.rewrite_real(content, _log) + if count: + with open(config_file, "w") as f: + f.write(new_content) + _log(f"Restored real acceptance in {count} speculative-config entries") + else: + _log("EVAL_ONLY=true: recipe already uses real acceptance") return 0 if not _enabled("SYNTHETIC_ACCEPTANCE"): return 0 - injector = get_injector(framework) if injector is None: sys.exit( "ERROR: SYNTHETIC_ACCEPTANCE=true but no synthetic-acceptance " @@ -145,7 +160,12 @@ def inject(config_file, framework): al = _resolve_al( content, injector, - os.path.join(os.path.dirname(__file__), "..", "benchmarks", "speedbench-reference-al.yaml"), + os.path.join( + os.path.dirname(__file__), + "..", + "benchmarks", + "speedbench-reference-al.yaml", + ), ) _log(f"Injecting synthetic acceptance (length={al}) into {config_file}") diff --git a/runners/launch_b200-dgxc.sh b/runners/launch_b200-dgxc.sh index 2cd2f2ee81..7e1c5d1b50 100644 --- a/runners/launch_b200-dgxc.sh +++ b/runners/launch_b200-dgxc.sh @@ -225,6 +225,9 @@ if [[ "$IS_MULTINODE" == "true" ]]; then cd "$SRT_REPO_DIR" || exit 1 git checkout sa-submission-q2-2026 fi + if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" || exit 1 + fi echo "Installing srtctl..." export UV_INSTALL_DIR="$GITHUB_WORKSPACE/.local/bin" @@ -364,6 +367,8 @@ EOF # so large-model loads (e.g. DSR1-FP8 ~680GB off shared FS) finish in time. # Uses ${CONFIG_FILE%%:*} because CONFIG_FILE may carry an :override[N] suffix. sed -i 's/^ max_attempts: [0-9]*/ max_attempts: 720/' "${CONFIG_FILE%%:*}" + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 SRTCTL_PREFLIGHT_ARGS=() # Kimi K2.6 weights are staged on the Slurm compute nodes, not the login node. diff --git a/runners/launch_gb200-nv.sh b/runners/launch_gb200-nv.sh index 34d7c4c806..870b684725 100755 --- a/runners/launch_gb200-nv.sh +++ b/runners/launch_gb200-nv.sh @@ -488,6 +488,9 @@ else git clone --branch cam/sa-submission-q2-2026 --single-branch https://github.com/cquil11/srt-slurm-nv.git "$SRT_REPO_DIR" cd "$SRT_REPO_DIR" fi +if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" || exit 1 +fi echo "Installing srtctl..." curl -LsSf https://astral.sh/uv/install.sh | sh @@ -635,10 +638,10 @@ if command -v squeue >/dev/null 2>&1; then fi sed -i "s/^name:.*/name: \"${SRT_SLURM_JOB_NAME}\"/" "$CONFIG_PATH" -# Optionally inject synthetic acceptance into the recipe's speculative-config -# when SYNTHETIC_ACCEPTANCE=true (no-op otherwise). Must run after the name -# override and before srtctl apply so the rendered job picks it up. -python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" "$CONFIG_PATH" "$FRAMEWORK" +# Restore real acceptance for eval-only jobs, or inject synthetic acceptance +# when a throughput run explicitly enables it. +python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 # Don't leak the login-node venv to the compute-node orchestrator. sbatch's # default --export=ALL propagates VIRTUAL_ENV (set by `source diff --git a/runners/launch_h200-dgxc-slurm.sh b/runners/launch_h200-dgxc-slurm.sh index 8991e04182..622fcb66f1 100755 --- a/runners/launch_h200-dgxc-slurm.sh +++ b/runners/launch_h200-dgxc-slurm.sh @@ -84,6 +84,9 @@ if [[ "$IS_MULTINODE" == "true" ]]; then cd "$SRT_REPO_DIR" git checkout sa-submission-q2-2026 fi + if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" + fi echo "Installing srtctl..." curl -LsSf https://astral.sh/uv/install.sh | sh @@ -206,6 +209,8 @@ EOF sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" sed -i '/^health_check:/,/^[^ ]/{ /^health_check:/d; /^ /d; }' "$CONFIG_PATH" printf '\nhealth_check:\n max_attempts: 720\n interval_seconds: 10\n' >> "$CONFIG_PATH" + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" WORKLOAD_TAG="${ISL}x${OSL}" if [[ "$IS_AGENTIC" == "1" ]]; then WORKLOAD_TAG="agentic" diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py new file mode 100755 index 0000000000..daa52d9ab0 --- /dev/null +++ b/runners/patch_srt_eval_dispatch.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Enable InferenceX-selected eval dispatch in an srt-slurm checkout.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +DO_SWEEP_ENV_BLOCK = """ "EVAL_ONLY", + "IS_MULTINODE",""" +DO_SWEEP_ENV_REPLACEMENT = """ "EVAL_ONLY", + "EVAL_FRAMEWORK", + "EVAL_SUITE", + "IS_MULTINODE",""" +LM_EVAL_COMMAND = 'run_eval --framework lm-eval --port "$PORT" || eval_rc=$?' +GENERIC_EVAL_COMMAND = 'run_eval --port "$PORT" || eval_rc=$?' +EVAL_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true +cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" +KIMI_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true +cp -v *_vendor_report.json /logs/eval_results/ 2>/dev/null || true +cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" + + +def prepare_replacements( + path: Path, + replacements: tuple[tuple[str, str], ...], +) -> tuple[str, str, bool]: + """Validate source replacements without mutating the checkout.""" + original = path.read_text() + content = original + changed = False + for old, new in replacements: + old_count = content.count(old) + new_count = content.count(new) + if old_count == 1 and new_count == 0: + content = content.replace(old, new, 1) + changed = True + elif old_count != 0 or new_count != 1: + raise RuntimeError( + f"invalid patch state in {path}: old anchor count={old_count}, " + f"replacement count={new_count}" + ) + return original, content, changed + + +def patch_checkout(root: Path) -> list[Path]: + """Patch both post-eval sources after validating the complete checkout.""" + patches = ( + ( + root / "src/srtctl/cli/do_sweep.py", + ((DO_SWEEP_ENV_BLOCK, DO_SWEEP_ENV_REPLACEMENT),), + ), + ( + root / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh", + ( + (LM_EVAL_COMMAND, GENERIC_EVAL_COMMAND), + (EVAL_ARTIFACT_COPY, KIMI_ARTIFACT_COPY), + ), + ), + ) + staged = [ + (path, *prepare_replacements(path, replacements)) + for path, replacements in patches + ] + changed = [] + written = [] + try: + for path, original, replacement, needs_write in staged: + if needs_write: + path.write_text(replacement) + written.append((path, original)) + changed.append(path) + except OSError: + for path, original in reversed(written): + path.write_text(original) + raise + return changed + + +def main(argv: list[str]) -> int: + """Patch the checkout named on the command line.""" + if len(argv) != 2: + print(f"Usage: {argv[0]} SRT_SLURM_CHECKOUT", file=sys.stderr) + return 2 + root = Path(argv[1]).resolve() + try: + changed = patch_checkout(root) + except (OSError, RuntimeError) as error: + print( + f"ERROR: failed to patch srt-slurm eval dispatch: {error}", file=sys.stderr + ) + return 1 + if changed: + for path in changed: + print(f"Patched srt-slurm eval dispatch: {path}") + else: + print("srt-slurm eval dispatch is already patched") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/runners/synthetic_injectors/vllm.py b/runners/synthetic_injectors/vllm.py index e71a5df2b4..d36740195c 100644 --- a/runners/synthetic_injectors/vllm.py +++ b/runners/synthetic_injectors/vllm.py @@ -1,13 +1,8 @@ -"""vLLM synthetic-acceptance backend (FRAMEWORK=dynamo-vllm). +"""vLLM speculative-acceptance recipe rewriting. -Rewrites every ``speculative-config: ''`` entry in an srt-slurm recipe to -use synthetic rejection sampling: it adds ``rejection_sample_method=synthetic`` -and ``synthetic_acceptance_length=`` to the JSON so the engine emits a -controlled mean acceptance length instead of running the real draft model. - -Registered under the "dynamo-vllm" framework key at import time, so importing -the ``synthetic_injectors`` package is enough for the generic driver to resolve -this backend. +Throughput opt-ins can inject synthetic acceptance. Eval-only runs restore real +block verification so model outputs remain valid for accuracy checks. The +backend is registered for both direct vLLM and Dynamo-vLLM recipes. """ import json @@ -57,7 +52,9 @@ def _replace(match): new_content, count = _SPEC_CONFIG_RE.subn(_replace, content) if count: - after = [ln.strip() for ln in new_content.splitlines() if _SPEC_CONFIG_RE.search(ln)] + after = [ + ln.strip() for ln in new_content.splitlines() if _SPEC_CONFIG_RE.search(ln) + ] if after: log("After:") for ln in after: @@ -66,4 +63,28 @@ def _replace(match): return new_content, count +def rewrite_real(content, log): + """Restore real block verification in every synthetic config entry.""" + modified = 0 + + def _replace(match): + nonlocal modified + spec = json.loads(match.group(1)) + if ( + spec.get("rejection_sample_method") != "synthetic" + and "synthetic_acceptance_length" not in spec + ): + return match.group(0) + spec["rejection_sample_method"] = "block" + spec.pop("synthetic_acceptance_length", None) + modified += 1 + return "speculative-config: '" + json.dumps(spec, separators=(",", ":")) + "'" + + new_content = _SPEC_CONFIG_RE.sub(_replace, content) + if modified: + log("Restored real block verification for eval-only mode") + return new_content, modified + + register("dynamo-vllm", sys.modules[__name__]) +register("vllm", sys.modules[__name__]) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index c027cc9b2f..bfce571816 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -1,9 +1,14 @@ +import json +import os import subprocess from pathlib import Path +import yaml REPO_ROOT = Path(__file__).resolve().parents[1] SLURM_UTILS = REPO_ROOT / "runners" / "slurm_utils.sh" +PATCH_SRT_EVAL = REPO_ROOT / "runners" / "patch_srt_eval_dispatch.py" +INJECT_ACCEPTANCE = REPO_ROOT / "runners" / "inject_synthetic_acceptance.py" def run_bash(command: str, *args: Path | str) -> subprocess.CompletedProcess[str]: @@ -55,3 +60,195 @@ def test_copy_agentic_results_fails_when_aggregate_is_missing( assert result.returncode != 0 assert "no run_conc*.json results found" in result.stderr + + +def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( + tmp_path: Path, +) -> None: + do_sweep = tmp_path / "src/srtctl/cli/do_sweep.py" + eval_script = tmp_path / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh" + do_sweep.parent.mkdir(parents=True) + eval_script.parent.mkdir(parents=True) + do_sweep.write_text( + " for var in [\n" + ' "RUN_EVAL",\n' + ' "EVAL_ONLY",\n' + ' "IS_MULTINODE",\n' + " ]:\n" + ) + eval_script.write_text( + 'run_eval --framework lm-eval --port "$PORT" || eval_rc=$?\n' + "cp -v results*.json /logs/eval_results/ 2>/dev/null || true\n" + "cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true\n" + ) + + first = subprocess.run( + ["python3", str(PATCH_SRT_EVAL), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + second = subprocess.run( + ["python3", str(PATCH_SRT_EVAL), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + + assert first.returncode == 0, first.stderr + assert second.returncode == 0, second.stderr + assert do_sweep.read_text().count('"EVAL_FRAMEWORK"') == 1 + assert do_sweep.read_text().count('"EVAL_SUITE"') == 1 + assert 'run_eval --port "$PORT"' in eval_script.read_text() + assert "--framework lm-eval" not in eval_script.read_text() + assert "*_vendor_report.json" in eval_script.read_text() + assert "already patched" in second.stdout + + +def test_patch_srt_eval_dispatch_preflights_before_writing(tmp_path: Path) -> None: + do_sweep = tmp_path / "src/srtctl/cli/do_sweep.py" + eval_script = tmp_path / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh" + do_sweep.parent.mkdir(parents=True) + eval_script.parent.mkdir(parents=True) + original_do_sweep = ' "EVAL_ONLY",\n "IS_MULTINODE",\n' + original_eval_script = "unsupported eval hook\n" + do_sweep.write_text(original_do_sweep) + eval_script.write_text(original_eval_script) + + result = subprocess.run( + ["python3", str(PATCH_SRT_EVAL), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert do_sweep.read_text() == original_do_sweep + assert eval_script.read_text() == original_eval_script + + +def test_patch_srt_eval_dispatch_rejects_mixed_patch_state(tmp_path: Path) -> None: + do_sweep = tmp_path / "src/srtctl/cli/do_sweep.py" + eval_script = tmp_path / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh" + do_sweep.parent.mkdir(parents=True) + eval_script.parent.mkdir(parents=True) + original_do_sweep = ( + ' "EVAL_ONLY",\n' + ' "IS_MULTINODE",\n' + ' "EVAL_ONLY",\n' + ' "EVAL_FRAMEWORK",\n' + ' "EVAL_SUITE",\n' + ' "IS_MULTINODE",\n' + ) + original_eval_script = ( + 'run_eval --framework lm-eval --port "$PORT" || eval_rc=$?\n' + "cp -v results*.json /logs/eval_results/ 2>/dev/null || true\n" + "cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true\n" + ) + do_sweep.write_text(original_do_sweep) + eval_script.write_text(original_eval_script) + + result = subprocess.run( + ["python3", str(PATCH_SRT_EVAL), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "invalid patch state" in result.stderr + assert do_sweep.read_text() == original_do_sweep + assert eval_script.read_text() == original_eval_script + + +def test_eval_only_restores_real_vllm_acceptance(tmp_path: Path) -> None: + recipe = tmp_path / "recipe.yaml" + recipe.write_text( + "speculative-config: " + """'{\"method\":\"dspark\",\"num_speculative_tokens\":2,""" + """\"rejection_sample_method\":\"synthetic\",""" + """\"synthetic_acceptance_length\":2.51}'\n""" + ) + env = { + **os.environ, + "EVAL_ONLY": "true", + "SYNTHETIC_ACCEPTANCE": "true", + } + + result = subprocess.run( + ["python3", str(INJECT_ACCEPTANCE), str(recipe), "vllm"], + env=env, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + rewritten = recipe.read_text() + assert '"rejection_sample_method":"block"' in rewritten + assert "synthetic_acceptance_length" not in rewritten + + +def test_eval_only_acceptance_rewrite_allows_non_speculative_recipe( + tmp_path: Path, +) -> None: + recipe = tmp_path / "recipe.yaml" + original = "backend:\n type: vllm\n" + recipe.write_text(original) + + result = subprocess.run( + ["python3", str(INJECT_ACCEPTANCE), str(recipe), "dynamo-vllm"], + env={**os.environ, "EVAL_ONLY": "true"}, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + assert recipe.read_text() == original + + +def test_nvidia_srt_launchers_prepare_kimi_eval_dispatch() -> None: + launchers = ( + REPO_ROOT / "runners/launch_h200-dgxc-slurm.sh", + REPO_ROOT / "runners/launch_b200-dgxc.sh", + REPO_ROOT / "runners/launch_gb200-nv.sh", + ) + + for launcher in launchers: + content = launcher.read_text() + assert "patch_srt_eval_dispatch.py" in content + assert 'EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor"' in content + assert "inject_synthetic_acceptance.py" in content + + +def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: + recipes = { + "agg-gb200-tep16-balanced-agentic.yaml": 96, + "agg-gb200-tp16-latency-agentic.yaml": 24, + } + recipe_dir = ( + REPO_ROOT / "benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" + ) + + for filename, largest_capture in recipes.items(): + recipe = yaml.safe_load((recipe_dir / filename).read_text()) + raw_config = recipe["backend"]["vllm_config"]["aggregated"][ + "compilation-config" + ] + compilation_config = json.loads(raw_config) + + assert compilation_config["cudagraph_capture_sizes"][-1] == largest_capture + assert compilation_config["pass_config"]["fuse_allreduce_rms"] is False + + +def test_b200_kimi_recipe_uses_available_roce_devices() -> None: + recipe_path = ( + REPO_ROOT + / "benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" + / "agg-b200-tp8pp2-agentic.yaml" + ) + recipe = yaml.safe_load(recipe_path.read_text()) + devices = recipe["backend"]["aggregated_environment"]["UCX_NET_DEVICES"] + + assert devices == ",".join(f"mlx5_{index}:1" for index in range(8)) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 58237912f0..d5de674c62 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -48,7 +48,8 @@ runner. Existing jobs continue to use lm-eval with GSM8K by default. The default eval framework is [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (`lm-eval`). Agentic eval-only matrix jobs inherit this default and therefore run the same GSM8K task as 8k1k. Explicit agentic runs can still select SWE-bench. -The Phase 1 Kimi smoke is opt-in and single-node only. Select +The Phase 1 Kimi smoke is opt-in. It supports single-node jobs and Kimi K3 +aggregate H200, B200, and GB200 srt-slurm jobs. Select `eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema` on `e2e-tests.yml`, or invoke it from the repository root after a server is ready: @@ -98,7 +99,10 @@ upstream pytest process to 900 seconds. This smoke validates one object-schema tool call. It does not cover tool choice, parallel calls, multi-turn execution, or general agent quality. Multi-value -batched concurrency and multi-node execution are unsupported. +batched concurrency is unsupported. Multi-node aggregate jobs run the same +two-case smoke against their OpenAI-compatible frontend. Eval-only launchers +restore real block verification before submitting recipes that otherwise use +synthetic acceptance for throughput. ### Benchmark script flow @@ -161,10 +165,10 @@ Multi-node evals support two hardware paths: **NVIDIA Slurm multi-node (GB200, GB300, B200, B300, H100, H200)** runs through [srt-slurm](https://github.com/NVIDIA/srt-slurm) on the `sa-submission-q2-2026` branch. - `do_sweep.py` skips the benchmark stage when `EVAL_ONLY=true`, runs `_run_post_eval()` directly - In eval-only mode, uses the full `wait_for_model()` health check (same as benchmark stage) since the benchmark health check was skipped -- `lm-eval` runner (`benchmarks/lm_eval.py`) is invoked by `do_sweep.py` as a post/eval-only step and sources InferenceX's `benchmark_lib.sh` from the mounted workspace (`/infmax-workspace`) +- The registered srt-slurm `lm-eval` post-runner sources InferenceX's `benchmark_lib.sh` from the mounted workspace (`/infmax-workspace`). Kimi-selected launches patch that hook to use generic `run_eval` dispatch while preserving lm-eval as the default. - Eval artifacts written to `/logs/eval_results/` inside the container, collected by launch scripts - NVIDIA Slurm launch scripts always collect server logs for debugging but skip benchmark result collection when `EVAL_ONLY=true` -- Env vars threaded: `RUN_EVAL`, `EVAL_ONLY`, `IS_MULTINODE`, `FRAMEWORK`, `PRECISION`, `MODEL_PREFIX`, `RUNNER_TYPE`, `RESULT_FILENAME`, `SPEC_DECODING`, `ISL`, `OSL`, `PREFILL_TP/EP/NUM_WORKERS/DP_ATTN`, `DECODE_TP/EP/NUM_WORKERS/DP_ATTN`, `MODEL_NAME`, `EVAL_CONC` +- Env vars threaded: `RUN_EVAL`, `EVAL_ONLY`, `EVAL_FRAMEWORK`, `EVAL_SUITE`, `IS_MULTINODE`, `FRAMEWORK`, `PRECISION`, `MODEL_PREFIX`, `RUNNER_TYPE`, `RESULT_FILENAME`, `SPEC_DECODING`, `ISL`, `OSL`, `PREFILL_TP/EP/NUM_WORKERS/DP_ATTN`, `DECODE_TP/EP/NUM_WORKERS/DP_ATTN`, `MODEL_NAME`, `EVAL_CONC` For multi-node `all-evals`, `EVAL_CONC` is a space-separated list. When it contains multiple values, `run_eval` runs those concurrency points sequentially against the same live engine, stages each result with a `_concN` filename suffix, and records expected/completed/failed points in `meta_env.json`. diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index fae4aab2de..bc235c094f 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -11,6 +11,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] BENCHMARK_LIB = REPO_ROOT / "benchmarks" / "benchmark_lib.sh" +MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" _SCRIPT = r''' @@ -221,6 +222,31 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: assert "FINAL_SUITE=unset" in result.stdout +def test_kimi_default_suite_reaches_eval_only_metadata() -> None: + script = r''' +source "$BENCHMARK_LIB" +run_kimi_vendor_eval() { echo "DISPATCH=$EVAL_SUITE"; } +append_lm_eval_summary() { echo "METADATA=$EVAL_SUITE"; } +export EVAL_FRAMEWORK=kimi-vendor +export EVAL_ONLY=true +export IS_AGENTIC=1 +export EVAL_CONCURRENT_REQUESTS="" +unset EVAL_SUITE +run_eval --port 8888 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "DISPATCH=kimi_tool_call_schema" in result.stdout + assert "METADATA=kimi_tool_call_schema" in result.stdout + + def test_kimi_vendor_rejects_batched_concurrency() -> None: result = _run_invalid_call( "EVAL_MAX_MODEL_LEN=16384 " @@ -239,15 +265,6 @@ def test_kimi_vendor_rejects_unsupported_suite() -> None: assert "unsupported Kimi Vendor Verifier suite 'gsm8k'" in result.stderr -def test_kimi_vendor_rejects_multinode() -> None: - for value in ("true", "1"): - result = _run_invalid_call( - f"EVAL_SUITE=kimi_tool_call_schema IS_MULTINODE={value} " - "run_kimi_vendor_eval" - ) - assert result.returncode == 2 - assert "supports single-node only" in result.stderr - def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, @@ -352,7 +369,9 @@ def test_kimi_vendor_surfaces_failure_artifact_error(tmp_path: Path) -> None: -def test_kimi_vendor_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None: +def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( + tmp_path: Path, +) -> None: results_dir = tmp_path / "results" verifier_dir = tmp_path / "verifier" runtime_dir = tmp_path / "runtime" @@ -383,7 +402,7 @@ def test_kimi_vendor_runner_uses_fixed_upstream_contract(tmp_path: Path) -> None "RUNTIME_DIR": str(runtime_dir), "OPENAI_API_KEY": "must-not-be-forwarded", "KV_OFFLOADING": "none", - "IS_MULTINODE": "false", + "IS_MULTINODE": "true", } for key in ( "EVAL_SUITE", @@ -1134,6 +1153,18 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" +def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + forwarded = workflow["jobs"]["test-sweep-multi-node-agentic-evals"]["with"] + reusable_workflow = yaml.safe_load(MULTINODE_WORKFLOW.read_text()) + + assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" + assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" + assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" + assert "*_vendor_report.json" in MULTINODE_WORKFLOW.read_text() + + def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) @@ -1148,4 +1179,7 @@ def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: if "rows.extend" in line ) - assert '"multinode_agentic_evals"' in flatten_command \ No newline at end of file + assert '"multinode_agentic_evals"' in flatten_command + get_jobs_command = get_jobs["run"] + assert "EVALS=$(" in get_jobs_command + assert "score_matrix eval" in get_jobs_command \ No newline at end of file From 13c5a457ae39c8e252ed57a99ba4bb114ca842da Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:43:29 -0500 Subject: [PATCH 17/99] fix: harden Kimi eval runtime failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:强化 Kimi 评估运行时故障处理 --- .../workflows/benchmark-multinode-tmpl.yml | 2 +- benchmarks/benchmark_lib.sh | 221 +++++++++-- utils/collect_eval_results.py | 47 ++- utils/evals/EVALS.md | 11 +- utils/evals/test_batched_eval.py | 100 +++++ utils/evals/test_run_eval_dispatch.py | 345 ++++++++++++++++++ utils/evals/validate_scores.py | 46 +++ utils/test_collect_eval_results.py | 85 +++-- 8 files changed, 796 insertions(+), 61 deletions(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 675394838a..7004faed99 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -479,7 +479,7 @@ jobs: if: ${{ always() && (env.RUN_EVAL == 'true' || inputs.eval-only) }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: eval_${{ env.EXP_NAME }}_${{ env.RESULT_FILENAME }} + name: eval_${{ env.EXP_NAME }}_${{ env.PRECISION }}_${{ env.FRAMEWORK }}_p${{ env.PREFILL_NUM_WORKERS }}x${{ env.PREFILL_TP }}p${{ env.PREFILL_PP_SIZE }}c${{ env.PREFILL_DCP_SIZE }}k${{ env.PREFILL_PCP_SIZE }}e${{ env.PREFILL_EP }}d${{ env.PREFILL_DP_ATTN }}_d${{ env.DECODE_NUM_WORKERS }}x${{ env.DECODE_TP }}p${{ env.DECODE_PP_SIZE }}c${{ env.DECODE_DCP_SIZE }}k${{ env.DECODE_PCP_SIZE }}e${{ env.DECODE_EP }}d${{ env.DECODE_DP_ATTN }}_kv${{ env.KV_OFFLOADING }}-${{ env.KV_OFFLOAD_BACKEND }}_spec${{ env.SPEC_DECODING }}_c${{ join(fromJson(inputs.conc-list), 'x') }}_${{ runner.name }} path: | meta_env.json results*.json diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 7b6454652a..cef240b274 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -856,31 +856,210 @@ _prepare_kimi_vendor_runtime() { _prepare_kimi_vendor_verifier() { local repo_url="$1" local verifier_ref="$2" - local checkout_dir + local checkout_dir prepare_rc=0 - command -v git >/dev/null 2>&1 || { - echo "ERROR: git is required to fetch Kimi-Vendor-Verifier" >&2 + checkout_dir="$(mktemp -d /tmp/kimi-vendor-verifier-XXXXXX)" || { + echo "ERROR: could not create a temporary directory for Kimi-Vendor-Verifier" >&2 return 1 } - checkout_dir="$(mktemp -d /tmp/kimi-vendor-verifier-XXXXXX)" || return $? - if ! ( - git init -q "$checkout_dir" \ - && git -C "$checkout_dir" remote add origin "$repo_url" \ - && git -C "$checkout_dir" fetch -q --filter=blob:none --depth=1 \ - origin "$verifier_ref" \ - && git -C "$checkout_dir" update-ref HEAD FETCH_HEAD \ - && git -C "$checkout_dir" sparse-checkout set --no-cone \ - /pyproject.toml \ - /tests/conftest.py \ - /tests/__init__.py \ - /tests/tool_call_json_schema/ \ - /testdata/walle_validator_cases/ \ - && git -C "$checkout_dir" checkout -q --detach HEAD - ); then - rm -rf "$checkout_dir" - echo "ERROR: failed to fetch Kimi-Vendor-Verifier at ${verifier_ref}" >&2 - return 1 + + python3 - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? +from pathlib import Path +import re +import socket +import sys +import tarfile +import tempfile +import time +from urllib.parse import quote, urlsplit, urlunsplit +from urllib.request import Request, urlopen + + +repo_url, verifier_ref, checkout_dir_arg = sys.argv[1:] +checkout_dir = Path(checkout_dir_arg) +stage = "derive the pinned archive URL" + + +def archive_member_parts(name): + if not name or "\x00" in name or "\\" in name or name.startswith("/"): + raise ValueError(f"unsafe archive member path: {name!r}") + normalized = name.rstrip("/") + parts = normalized.split("/") + if not normalized or any(part in ("", ".", "..") for part in parts): + raise ValueError(f"unsafe archive member path: {name!r}") + return tuple(parts) + + +try: + if not re.fullmatch(r"[0-9a-fA-F]{40}", verifier_ref): + raise ValueError(f"expected a 40-character commit SHA, got {verifier_ref!r}") + + parsed_repo_url = urlsplit(repo_url) + if parsed_repo_url.scheme not in ("http", "https") or not parsed_repo_url.netloc: + raise ValueError(f"unsupported repository URL: {repo_url!r}") + if parsed_repo_url.query or parsed_repo_url.fragment: + raise ValueError(f"repository URL must not contain a query or fragment: {repo_url!r}") + repo_path = parsed_repo_url.path.rstrip("/") + if repo_path.endswith(".git"): + repo_path = repo_path[:-4] + if not repo_path: + raise ValueError(f"repository URL has no repository path: {repo_url!r}") + archive_path = f"{repo_path}/archive/{quote(verifier_ref, safe='')}.tar.gz" + archive_url = urlunsplit( + (parsed_repo_url.scheme, parsed_repo_url.netloc, archive_path, "", "") + ) + + stage = f"download {archive_url}" + request = Request( + archive_url, + headers={"User-Agent": "InferenceX-Kimi-Vendor-Verifier"}, + ) + with tempfile.TemporaryFile() as archive_file: + downloaded = 0 + deadline = time.monotonic() + 60 + with urlopen(request, timeout=60) as response: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("archive download exceeded the 60-second deadline") + sock = getattr(getattr(response, "fp", None), "raw", None) + sock = getattr(sock, "_sock", None) + if sock is not None: + sock.settimeout(max(0.001, remaining)) + try: + chunk = response.read(1024 * 1024) + except socket.timeout as error: + raise TimeoutError( + "archive download exceeded the 60-second deadline" + ) from error + if not chunk: + break + downloaded += len(chunk) + if downloaded > 128 * 1024 * 1024: + raise ValueError("archive download exceeds the 128 MiB safety limit") + archive_file.write(chunk) + if downloaded == 0: + raise ValueError("downloaded archive is empty") + archive_file.seek(0) + + stage = "validate the downloaded archive" + required_files = { + "pyproject.toml", + "tests/conftest.py", + "tests/__init__.py", + "tests/tool_call_json_schema/conftest.py", + "tests/tool_call_json_schema/__init__.py", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "tests/tool_call_json_schema/validator.py", + "testdata/walle_validator_cases/validator_cases/TestAdditionalProperties/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestAnyOf/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestBasicTypes/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestDefs/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestDescription/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestEnforcerCases/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestID/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestKeywordsValidation/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestNestedDefsDepth/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestNumberFormat/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestRangeConstraints/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestRefInProperties/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestReferences/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestRequired/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestSingleTypeInArray/valid.jsonl", + "testdata/walle_validator_cases/validator_cases/TestTypeLocation/valid.jsonl", + } + selected_files = {} + archive_roots = set() + member_count = 0 + archive_size = 0 + selected_size = 0 + + with tarfile.open(fileobj=archive_file, mode="r|gz") as archive: + for member in archive: + member_count += 1 + if member_count > 100_000: + raise ValueError("archive contains more than 100000 members") + if member.size < 0: + raise ValueError( + f"archive member has a negative size: {member.name!r}" + ) + archive_size += member.size + if archive_size > 512 * 1024 * 1024: + raise ValueError("expanded archive exceeds the 512 MiB safety limit") + + parts = archive_member_parts(member.name) + archive_roots.add(parts[0]) + if len(archive_roots) > 1: + roots = ", ".join(sorted(archive_roots)) + raise ValueError(f"archive has multiple roots: {roots}") + if not (member.isdir() or member.isfile()): + raise ValueError( + f"archive member has unsafe type: {member.name!r}" + ) + if len(parts) == 1: + continue + + relative_path = "/".join(parts[1:]) + if relative_path not in required_files: + continue + if relative_path in selected_files: + raise ValueError( + f"archive contains duplicate selected path: {relative_path!r}" + ) + if not member.isfile(): + raise ValueError( + f"required path is not a regular file: {relative_path}" + ) + selected_size += member.size + if selected_size > 256 * 1024 * 1024: + raise ValueError( + "selected archive subset exceeds the 256 MiB safety limit" + ) + source = archive.extractfile(member) + if source is None: + raise ValueError(f"could not read archive member: {member.name!r}") + with source: + content = source.read(member.size + 1) + if len(content) != member.size: + raise ValueError( + f"archive member size mismatch: {member.name!r}" + ) + selected_files[relative_path] = content + + if member_count == 0: + raise ValueError("archive contains no members") + if len(archive_roots) != 1: + raise ValueError("archive does not have exactly one root") + missing_files = sorted(required_files - selected_files.keys()) + if missing_files: + raise ValueError( + "archive is missing required files: " + ", ".join(missing_files) + ) + + stage = "extract the verified archive subset" + if any(checkout_dir.iterdir()): + raise ValueError(f"checkout directory is not empty: {checkout_dir}") + for relative_path, content in selected_files.items(): + destination = checkout_dir.joinpath(*relative_path.split("/")) + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("xb") as output: + output.write(content) +except Exception as error: + print( + f"ERROR: failed to {stage} for Kimi-Vendor-Verifier " + f"at {verifier_ref}: {error}", + file=sys.stderr, + ) + raise SystemExit(1) +PY + + if [ "$prepare_rc" -ne 0 ]; then + if ! rm -rf "$checkout_dir"; then + echo "ERROR: failed to remove partial Kimi-Vendor-Verifier directory ${checkout_dir}" >&2 + fi + return "$prepare_rc" fi + printf '%s\n' "$checkout_dir" } diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 28d9ab11c9..545ef3a855 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys import json +import math import re from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -72,10 +73,10 @@ def result_concurrency(path: Path) -> Optional[int]: def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: - """Return collector-compatible eval result JSONs from one artifact directory. + """Return the latest collector-compatible eval result JSONs. - Legacy lm-eval artifacts contribute their latest result file. Batched - artifacts contribute the latest result file for each `_concN` suffix. + Result filenames contain sortable timestamps. Mtime remains a fallback for + legacy names, with the filename as a deterministic tie-breaker. """ immediate_jsons = set(d.glob('results*.json')) immediate_jsons.update( @@ -83,6 +84,17 @@ def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: ) lm_paths = [] + def recency_key(path: Path) -> Tuple[str, int, str]: + match = re.search( + r"\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d+)?", + path.name, + ) + return ( + match.group(0) if match else "", + path.stat().st_mtime_ns, + path.name, + ) + for p in immediate_jsons: data = load_json(p) if not isinstance(data, dict): @@ -93,7 +105,7 @@ def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: if not lm_paths: return [] if not batched: - return [max(lm_paths, key=lambda path: path.stat().st_mtime)] + return [max(lm_paths, key=recency_key)] latest_by_conc: Dict[int, Path] = {} for path in lm_paths: @@ -101,15 +113,28 @@ def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: if conc is None: continue current = latest_by_conc.get(conc) - if current is None or path.stat().st_mtime > current.stat().st_mtime: + if current is None or recency_key(path) > recency_key(current): latest_by_conc[conc] = path return [latest_by_conc[conc] for conc in sorted(latest_by_conc)] -def detect_eval_jsons(d: Path) -> Tuple[Optional[Path], Optional[Path]]: - """Return the latest legacy lm-eval JSON and deprecated second slot.""" - lm_paths = detect_lm_eval_jsons(d) - return (lm_paths[0] if lm_paths else None), None +def has_invalid_effective_count(data: Dict[str, Any], task: str) -> bool: + """Return whether a task has an explicitly invalid effective count.""" + if 'n-samples' not in data: + return False + sample_counts = data['n-samples'] + if not isinstance(sample_counts, dict) or task not in sample_counts: + return True + task_samples = sample_counts[task] + if not isinstance(task_samples, dict) or 'effective' not in task_samples: + return True + effective = task_samples['effective'] + return ( + isinstance(effective, bool) + or not isinstance(effective, (int, float)) + or not math.isfinite(effective) + or effective <= 0 + ) def extract_lm_metrics(json_path: Path) -> List[Dict[str, Any]]: @@ -124,6 +149,8 @@ def extract_lm_metrics(json_path: Path) -> List[Dict[str, Any]]: - Values from results[task][metric,filter] """ data = load_json(json_path) or {} + if 'integration_error' in data: + return [] results = data.get('results', {}) configs = data.get('configs', {}) @@ -133,6 +160,8 @@ def extract_lm_metrics(json_path: Path) -> List[Dict[str, Any]]: extracted = [] for task in results.keys(): + if has_invalid_effective_count(data, task): + continue task_results = results[task] task_config = configs.get(task, {}) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index d5de674c62..74c4974c1c 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -75,10 +75,11 @@ launch their existing `*_mtp.sh` server instead of silently falling back to STP. The smoke runs the unmodified [MoonshotAI/Kimi-Vendor-Verifier](https://github.com/MoonshotAI/Kimi-Vendor-Verifier) -at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run creates a fresh -sparse checkout containing the upstream pytest configuration, tool-call schema -tests, and bundled Walle cases. InferenceX does not install the verifier package -or reimplement its request, streaming, retry, or validation logic. +at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run downloads the +fresh pinned GitHub source archive and safely extracts only the upstream pytest +configuration, tool-call schema tests, and bundled Walle cases. InferenceX does +not install the verifier package or reimplement its request, streaming, retry, +or validation logic. Python 3.12 or newer is required. The runner installs the minimal pinned runtime (`httpx[http2]`, `openai`, `jsonschema`, and `pytest`) into a temporary isolated @@ -137,7 +138,7 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | -| `_prepare_kimi_vendor_verifier` | Fetches a fresh pinned sparse checkout | +| `_prepare_kimi_vendor_verifier` | Downloads and safely extracts a fresh subset of the pinned source archive | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | diff --git a/utils/evals/test_batched_eval.py b/utils/evals/test_batched_eval.py index a5d6df0085..56c219e558 100644 --- a/utils/evals/test_batched_eval.py +++ b/utils/evals/test_batched_eval.py @@ -227,6 +227,106 @@ def test_validate_scores_checks_threshold_for_every_concurrency( assert "FAIL: [conc=4] gsm8k exact_match,strict-match" in captured.err +def test_validate_scores_reports_integration_failure_without_thresholding( + tmp_path: Path, + monkeypatch, + capsys, +) -> None: + result_path = tmp_path / "results_test.json" + result_path.write_text(json.dumps({ + "integration_error": { + "type": "RuntimeError", + "message": "vendor verifier checkout failed", + }, + "results": { + "gsm8k": { + "exact_match,strict-match": 0.0, + }, + }, + "n-samples": {"gsm8k": {"effective": 0}}, + })) + monkeypatch.setattr(sys, "argv", [ + "validate_scores.py", + "--meta-env", + str(tmp_path / "meta_env.json"), + "--results-glob", + str(result_path), + ]) + + assert validate_scores_main() == 1 + captured = capsys.readouterr() + assert "integration failure: RuntimeError: vendor verifier checkout failed" in captured.err + assert "gsm8k exact_match,strict-match" not in captured.err + + +def test_validate_scores_rejects_invalid_effective_count_without_thresholding( + tmp_path: Path, + monkeypatch, + capsys, +) -> None: + result_path = tmp_path / "results_test.json" + result_path.write_text(json.dumps({ + "results": { + "gsm8k": { + "exact_match,strict-match": 0.0, + }, + "other": { + "exact_match,strict-match": 0.0, + }, + "nonfinite": { + "exact_match,strict-match": 1.0, + }, + }, + "n-samples": { + "gsm8k": {"effective": "unknown"}, + "other": {"effective": 0}, + "nonfinite": {"effective": float("inf")}, + }, + })) + monkeypatch.setattr(sys, "argv", [ + "validate_scores.py", + "--meta-env", + str(tmp_path / "meta_env.json"), + "--results-glob", + str(result_path), + ]) + + assert validate_scores_main() == 1 + captured = capsys.readouterr() + assert "gsm8k invalid effective sample count: 'unknown'" in captured.err + assert "gsm8k exact_match,strict-match" not in captured.err + assert "other invalid effective sample count: 0" in captured.err + assert "other exact_match,strict-match" not in captured.err + + assert "nonfinite invalid effective sample count: inf" in captured.err + assert "nonfinite exact_match,strict-match" not in captured.err + +def test_validate_scores_accepts_legacy_result_without_effective_count( + tmp_path: Path, + monkeypatch, + capsys, +) -> None: + result_path = tmp_path / "results_test.json" + result_path.write_text(json.dumps({ + "results": { + "gsm8k": { + "exact_match,strict-match": 1.0, + }, + }, + })) + monkeypatch.setattr(sys, "argv", [ + "validate_scores.py", + "--meta-env", + str(tmp_path / "meta_env.json"), + "--results-glob", + str(result_path), + ]) + + assert validate_scores_main() == 0 + captured = capsys.readouterr() + assert "PASS: gsm8k exact_match,strict-match" in captured.out + + def test_amd_multinode_container_forwards_eval_concurrency_list() -> None: job_slurm = ( Path(__file__).resolve().parents[2] diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index bc235c094f..6344ee9247 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1,10 +1,16 @@ from __future__ import annotations +import io import json import os +import re import stat import subprocess +import tarfile +import threading +from contextlib import contextmanager +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path import yaml @@ -317,6 +323,166 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( assert not (results_dir / "kimi_vendor_report.json").exists() +_KIMI_VERIFIER_REQUIRED_FILES = { + "pyproject.toml", + "tests/conftest.py", + "tests/__init__.py", + "tests/tool_call_json_schema/conftest.py", + "tests/tool_call_json_schema/__init__.py", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "tests/tool_call_json_schema/validator.py", + *{ + f"testdata/walle_validator_cases/validator_cases/{case}/valid.jsonl" + for case in ( + "TestAdditionalProperties", + "TestAnyOf", + "TestBasicTypes", + "TestDefs", + "TestDescription", + "TestEnforcerCases", + "TestID", + "TestKeywordsValidation", + "TestNestedDefsDepth", + "TestNumberFormat", + "TestRangeConstraints", + "TestRefInProperties", + "TestReferences", + "TestRequired", + "TestSingleTypeInArray", + "TestTypeLocation", + ) + }, +} + + +def _kimi_verifier_archive( + *, + missing: str | None = None, + unsafe_member: tarfile.TarInfo | None = None, +) -> bytes: + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w:gz") as archive: + for relative_path in sorted(_KIMI_VERIFIER_REQUIRED_FILES - {missing}): + payload = relative_path.encode() + member = tarfile.TarInfo(f"verifier-pinned/{relative_path}") + member.size = len(payload) + archive.addfile(member, io.BytesIO(payload)) + extra = b"must not be extracted" + member = tarfile.TarInfo("verifier-pinned/README.md") + member.size = len(extra) + archive.addfile(member, io.BytesIO(extra)) + if unsafe_member is not None: + archive.addfile( + unsafe_member, + io.BytesIO(b"unsafe") if unsafe_member.isfile() else None, + ) + return output.getvalue() + + +@contextmanager +def _serve_archive(payload: bytes): + request_paths = [] + class ArchiveHandler(BaseHTTPRequestHandler): + def do_GET(self): + request_paths.append(self.path) + self.send_response(200) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, *args): + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), ArchiveHandler) + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield ( + f"http://127.0.0.1:{server.server_port}/owner/verifier.git", + request_paths, + ) + finally: + server.shutdown() + thread.join() + server.server_close() + + +def _prepare_local_kimi_verifier( + tmp_path: Path, + payload: bytes, + verifier_ref: str = "1" * 40, +) -> tuple[subprocess.CompletedProcess[str], Path, list[str]]: + checkout = tmp_path / "checkout" + script = r''' +source "$BENCHMARK_LIB" +git() { echo "git must not be invoked" >&2; return 127; } +mktemp() { mkdir "$CHECKOUT"; printf '%s\n' "$CHECKOUT"; } +_prepare_kimi_vendor_verifier "$REPO_URL" "$VERIFIER_REF" +''' + with _serve_archive(payload) as (repo_url, request_paths): + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "CHECKOUT": str(checkout), + "REPO_URL": repo_url, + "VERIFIER_REF": verifier_ref, + }, + text=True, + capture_output=True, + ) + return result, checkout, request_paths + + +def test_kimi_vendor_verifier_fetches_expected_subset_without_git(tmp_path: Path) -> None: + result, checkout, request_paths = _prepare_local_kimi_verifier( + tmp_path, + _kimi_verifier_archive(), + ) + verifier_ref = "1" * 40 + + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == str(checkout) + assert request_paths == [ + f"/owner/verifier/archive/{verifier_ref}.tar.gz", + ] + assert { + path.relative_to(checkout).as_posix() + for path in checkout.rglob("*") + if path.is_file() + } == _KIMI_VERIFIER_REQUIRED_FILES + assert "git must not be invoked" not in result.stderr + + +def test_kimi_vendor_verifier_removes_partial_checkout_when_member_missing( + tmp_path: Path, +) -> None: + missing = "tests/tool_call_json_schema/validator.py" + result, checkout, _ = _prepare_local_kimi_verifier( + tmp_path, + _kimi_verifier_archive(missing=missing), + ) + + assert result.returncode == 1 + assert missing in result.stderr + assert not checkout.exists() + + +def test_kimi_vendor_verifier_rejects_unsafe_archive_members(tmp_path: Path) -> None: + unsafe = tarfile.TarInfo("verifier-pinned/../../escaped") + unsafe.size = len(b"unsafe") + result, checkout, _ = _prepare_local_kimi_verifier( + tmp_path, + _kimi_verifier_archive(unsafe_member=unsafe), + ) + + assert result.returncode == 1 + assert "unsafe archive member path" in result.stderr + assert not checkout.exists() + assert not (tmp_path / "escaped").exists() + + def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: runtime_dir = tmp_path / "runtime" script = r''' @@ -1017,6 +1183,185 @@ def test_agentic_eval_limit_full_runs_whole_split(tmp_path): assert "GEN_RC=0" in res.stdout, res.stdout + res.stderr +def test_multinode_eval_artifact_names_are_bounded_and_distinct() -> None: + workflow = yaml.safe_load(MULTINODE_WORKFLOW.read_text()) + upload = next( + step + for step in workflow["jobs"]["benchmark"]["steps"] + if step.get("name") == "Upload eval results (if any)" + ) + expression = upload["with"]["name"] + assert expression.startswith("eval_") + assert "RESULT_FILENAME" not in expression + + targets = [ + { + "EXP_NAME": "kimik3_p2x16ep32dpa_d0x16ep32dpa_conc12", + "PRECISION": "fp4", + "FRAMEWORK": "vllm", + "PREFILL_NUM_WORKERS": "2", + "PREFILL_TP": "16", + "PREFILL_PP_SIZE": "1", + "SPEC_DECODING": "mtp", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "32", + "PREFILL_DP_ATTN": "true", + "DECODE_NUM_WORKERS": "0", + "DECODE_TP": "16", + "DECODE_PP_SIZE": "1", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "32", + "DECODE_DP_ATTN": "true", + "KV_OFFLOADING": "none", + "KV_OFFLOAD_BACKEND": "", + "conc-list": ["1", "12", "16"], + "runner.name": "h200-dgxc-slurm_00", + }, + { + "EXP_NAME": "kimik3_p4x8ep32dpa_d0x8ep32dpa_conc12", + "PRECISION": "fp4", + "FRAMEWORK": "vllm", + "PREFILL_NUM_WORKERS": "4", + "PREFILL_TP": "8", + "PREFILL_PP_SIZE": "1", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "32", + "PREFILL_DP_ATTN": "true", + "DECODE_NUM_WORKERS": "0", + "SPEC_DECODING": "mtp", + "DECODE_TP": "8", + "DECODE_PP_SIZE": "1", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "32", + "DECODE_DP_ATTN": "true", + "KV_OFFLOADING": "none", + "KV_OFFLOAD_BACKEND": "", + "conc-list": ["1", "12", "16"], + "runner.name": "h200-dgxc-slurm_01", + }, + { + "EXP_NAME": "kimik3_p4x8ep32dpa_d0x8ep32dpa_conc12_kvdram-vllm-simple", + "PRECISION": "fp4", + "FRAMEWORK": "vllm", + "PREFILL_NUM_WORKERS": "4", + "PREFILL_TP": "8", + "PREFILL_PP_SIZE": "1", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "32", + "PREFILL_DP_ATTN": "true", + "DECODE_NUM_WORKERS": "0", + "DECODE_TP": "8", + "DECODE_PP_SIZE": "1", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "32", + "SPEC_DECODING": "mtp", + "DECODE_DP_ATTN": "true", + "KV_OFFLOADING": "dram", + "KV_OFFLOAD_BACKEND": "vllm-simple", + "conc-list": ["1", "12", "16"], + "runner.name": "h200-dgxc-slurm_02", + }, + { + "EXP_NAME": "kimik3_p1x8_d0x8_conc16", + "PRECISION": "fp4", + "FRAMEWORK": "dynamo-vllm", + "PREFILL_NUM_WORKERS": "1", + "PREFILL_TP": "8", + "PREFILL_PP_SIZE": "2", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "1", + "PREFILL_DP_ATTN": "false", + "DECODE_NUM_WORKERS": "0", + "DECODE_TP": "8", + "DECODE_PP_SIZE": "2", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "1", + "DECODE_DP_ATTN": "false", + "SPEC_DECODING": "mtp", + "KV_OFFLOADING": "none", + "KV_OFFLOAD_BACKEND": "", + "conc-list": ["1", "16"], + "runner.name": "b200-dgxc_00", + }, + { + "EXP_NAME": "kimik3_p1x16ep16_d0x16ep16_conc16", + "PRECISION": "fp4", + "FRAMEWORK": "dynamo-vllm", + "PREFILL_NUM_WORKERS": "1", + "PREFILL_TP": "16", + "PREFILL_PP_SIZE": "1", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "16", + "PREFILL_DP_ATTN": "false", + "DECODE_NUM_WORKERS": "0", + "DECODE_TP": "16", + "DECODE_PP_SIZE": "1", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "16", + "DECODE_DP_ATTN": "false", + "KV_OFFLOADING": "none", + "SPEC_DECODING": "mtp", + "KV_OFFLOAD_BACKEND": "", + "conc-list": ["16"], + "runner.name": "gb200-nv_00", + }, + { + "EXP_NAME": "kimik3_p1x16_d0x16_conc1", + "PRECISION": "fp4", + "FRAMEWORK": "dynamo-vllm", + "PREFILL_NUM_WORKERS": "1", + "PREFILL_TP": "16", + "PREFILL_PP_SIZE": "1", + "PREFILL_DCP_SIZE": "1", + "PREFILL_PCP_SIZE": "1", + "PREFILL_EP": "1", + "PREFILL_DP_ATTN": "false", + "DECODE_NUM_WORKERS": "0", + "DECODE_TP": "16", + "DECODE_PP_SIZE": "1", + "DECODE_DCP_SIZE": "1", + "DECODE_PCP_SIZE": "1", + "DECODE_EP": "1", + "DECODE_DP_ATTN": "false", + "KV_OFFLOADING": "none", + "KV_OFFLOAD_BACKEND": "", + "SPEC_DECODING": "mtp", + "conc-list": ["1"], + "runner.name": "gb200-nv_01", + }, + ] + non_mtp_twin = {**targets[3], "SPEC_DECODING": "none"} + + def render(values: dict[str, object]) -> str: + name = expression + name = re.sub( + r"\$\{\{ join\(fromJson\(inputs\.conc-list\), 'x'\) \}\}", + "x".join(values["conc-list"]), + name, + ) + for key, value in values.items(): + if key != "conc-list": + name = name.replace(f"${{{{ env.{key} }}}}", str(value)) + name = name.replace("${{ runner.name }}", str(values["runner.name"])) + assert "${{" not in name + return name + + names = [render(target) for target in targets] + assert len(names) == len(set(names)) == 6 + assert render(targets[3]) != render(non_mtp_twin) + assert all(name.startswith("eval_") and len(name.encode()) <= 256 for name in names) + + _GENMODE_SCRIPT = r''' source "$BENCHMARK_LIB" 2>/dev/null diff --git a/utils/evals/validate_scores.py b/utils/evals/validate_scores.py index ba7fc13962..bf4b391728 100644 --- a/utils/evals/validate_scores.py +++ b/utils/evals/validate_scores.py @@ -5,6 +5,7 @@ import argparse import glob import json +import math import os import re import sys @@ -68,6 +69,34 @@ def resolve_threshold(config: dict, prefix: str | None, task: str, fallback: flo return default[task], "default" return fallback, "min-score" +def invalid_effective_count(data: dict, task: str) -> tuple[bool, object]: + """Return whether an explicitly present effective count is invalid.""" + if "n-samples" not in data: + return False, None + sample_counts = data["n-samples"] + if not isinstance(sample_counts, dict) or task not in sample_counts: + return True, sample_counts + task_samples = sample_counts[task] + if not isinstance(task_samples, dict) or "effective" not in task_samples: + return True, task_samples + effective = task_samples["effective"] + invalid = ( + isinstance(effective, bool) + or not isinstance(effective, (int, float)) + or not math.isfinite(effective) + or effective <= 0 + ) + return invalid, effective + + +def integration_error_message(error: object) -> str: + """Render the structured integration error fields for a direct failure.""" + if isinstance(error, dict): + error_type = error.get("type", "unknown") + message = error.get("message", "") + return f"{error_type}: {message}" + return f"unknown: {error}" + def validate_batch_manifest( meta_env_path: str, @@ -277,7 +306,24 @@ def main() -> int: conc_label = f"[conc={match.group(1)}] " if match else "" with open(f) as fh: data = json.load(fh) + if "integration_error" in data: + print( + f"FAIL: {conc_label}integration failure: " + f"{integration_error_message(data['integration_error'])}", + file=sys.stderr, + ) + failed = True + continue for task, metrics in data.get("results", {}).items(): + invalid_effective, effective = invalid_effective_count(data, task) + if invalid_effective: + print( + f"FAIL: {conc_label}{task} invalid effective sample count: " + f"{effective!r}", + file=sys.stderr, + ) + failed = True + continue min_score, source = resolve_threshold(config, prefix, task, args.min_score) for name, val in metrics.items(): if not name.startswith(args.metric_prefix) or "stderr" in name: diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 41cd5cf3d0..3cac4d6b48 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -1,14 +1,12 @@ """Tests for eval result aggregation.""" import json -import sys from pathlib import Path from collect_eval_results import ( EVAL_RESULT_FORMAT, build_row, collect_eval_rows, - main as collect_main, ) from evals.kimi_vendor_eval import RESULT_FORMAT as KIMI_VENDOR_RESULT_FORMAT @@ -153,39 +151,76 @@ def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None assert rows[0]["eval_suite"] == "provider_smoke" -def test_main_renders_zero_effective_samples( +def test_collect_eval_rows_excludes_integration_and_sample_failures( tmp_path: Path, - monkeypatch, - capsys, ) -> None: - for name, is_multinode in (("single", False), ("multi", True)): + for name, invalid in ( + ("integration", "integration"), + ("zero", 0), + ("nonnumeric", "unknown"), + ("nonfinite", float("nan")), + ("malformed", []), + ): artifact_dir = tmp_path / f"eval_{name}" artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text(json.dumps({ - "is_multinode": is_multinode, "eval_suite": "gsm8k", })) result_path = artifact_dir / f"results_{name}.json" _write_lm_eval_result(result_path, 0.0) result = json.loads(result_path.read_text()) - result["n-samples"]["gsm8k"]["effective"] = 0 + if invalid == "integration": + result["integration_error"] = { + "type": "RuntimeError", + "message": "vendor verifier checkout failed", + } + else: + result["n-samples"]["gsm8k"]["effective"] = invalid result_path.write_text(json.dumps(result)) - monkeypatch.chdir(tmp_path) - monkeypatch.setattr( - sys, - "argv", - ["collect_eval_results.py", str(tmp_path), "zero-samples"], - ) + assert collect_eval_rows(tmp_path) == [] + + +def test_collect_eval_rows_accepts_legacy_missing_effective_count( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_legacy" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text(json.dumps({ + "eval_suite": "gsm8k", + })) + result_path = artifact_dir / "results_legacy.json" + _write_lm_eval_result(result_path, 0.9) + result = json.loads(result_path.read_text()) + result.pop("n-samples") + result_path.write_text(json.dumps(result)) - collect_main() - - task_rows = [ - line - for line in capsys.readouterr().out.splitlines() - if "| gsm8k " in line - ] - assert len(task_rows) == 2 - for row in task_rows: - cells = [cell.strip() for cell in row.split("|")[1:-1]] - assert cells[-2] == "0" \ No newline at end of file + rows = collect_eval_rows(tmp_path) + + assert len(rows) == 1 + assert rows[0]["score"] == 0.9 + assert rows[0]["n_eff"] is None + + +def test_collect_eval_rows_does_not_resurrect_stale_valid_result( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_retry" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text(json.dumps({ + "eval_suite": "gsm8k", + })) + stale_path = artifact_dir / "results_older.json" + _write_lm_eval_result(stale_path, 1.0) + current_path = artifact_dir / "results_current.json" + _write_lm_eval_result(current_path, 0.0) + result = json.loads(current_path.read_text()) + result["integration_error"] = { + "type": "RuntimeError", + "message": "vendor verifier checkout failed", + } + current_path.write_text(json.dumps(result)) + stale_path.touch() + current_path.touch() + + assert collect_eval_rows(tmp_path) == [] \ No newline at end of file From 134906e56d97408b8b457f06e60fce6fc16030ba Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:36:04 -0500 Subject: [PATCH 18/99] test: capture Kimi response diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:捕获 Kimi 工具调用响应诊断数据。 --- .../workflows/benchmark-multinode-tmpl.yml | 7 + .github/workflows/e2e-tests.yml | 11 ++ benchmarks/benchmark_lib.sh | 5 + utils/evals/kimi_vendor_eval.py | 157 +++++++++++++++++- utils/evals/test_kimi_vendor_eval.py | 67 ++++++++ 5 files changed, 246 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 7004faed99..4f2462d137 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -148,6 +148,11 @@ on: type: string required: false default: "" + kimi-vendor-diagnostic: + description: "Capture ordered raw Kimi tool-call responses" + type: boolean + required: false + default: false eval-limit: description: "Eval instance count: empty/full = whole split (default); N = first-N smoke slice" required: false @@ -246,6 +251,7 @@ env: EVAL_SUITE: ${{ inputs.eval-suite }} EVAL_CONC: ${{ inputs.eval-conc }} EVAL_LIMIT: ${{ inputs.eval-limit }} + KIMI_VENDOR_DIAGNOSTIC: ${{ inputs.kimi-vendor-diagnostic && '1' || '0' }} SWEBENCH_GEN_MODE: ${{ inputs.swebench-gen-mode }} # GPU/multi-node runners lack Docker for SWE-bench scoring. SWEBENCH_USE_MODAL: 'true' @@ -484,6 +490,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a0a31b5e91..8d354e6027 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,6 +55,11 @@ on: required: false type: string default: "" + kimi-vendor-diagnostic: + description: "Capture ordered raw Kimi tool-call responses" + required: false + type: boolean + default: false swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -145,6 +150,11 @@ on: required: false type: string default: "" + kimi-vendor-diagnostic: + description: "Capture ordered raw Kimi tool-call responses" + required: false + type: boolean + default: false swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -601,6 +611,7 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} + kimi-vendor-diagnostic: ${{ inputs.kimi-vendor-diagnostic }} scenario-type: agentic-coding ref: ${{ inputs.ref }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index cef240b274..21bae9908e 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1150,6 +1150,10 @@ _run_kimi_tool_call_schema_eval() { fi local eval_rc=0 + local diagnostic_args=() + if [ "${KIMI_VENDOR_DIAGNOSTIC:-0}" = "1" ]; then + diagnostic_args=(--diagnostic) + fi PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ python3 "$adapter_path" \ --verifier-dir "$checkout_dir" \ @@ -1157,6 +1161,7 @@ _run_kimi_tool_call_schema_eval() { --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ + "${diagnostic_args[@]}" \ || eval_rc=$? _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" return "$eval_rc" diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 4f3debc6fb..9f791b318e 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -4,9 +4,10 @@ from __future__ import annotations import argparse +import importlib import json -import subprocess import sys +import subprocess from collections.abc import Mapping, Sequence from datetime import datetime, timezone from pathlib import Path @@ -19,6 +20,145 @@ DEFAULT_TIMEOUT_SECONDS = 900 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" +DIAGNOSTIC_REPORT_FILENAME = "eval_kimi_tool_call_diagnostic.json" +DIAGNOSTIC_MODES = ( + "non-stream", + "non-stream", + "stream", + "stream", + "stream", + "non-stream", +) + + +def _jsonable(value: Any) -> Any: + """Convert an OpenAI response model into JSON-compatible data.""" + if hasattr(value, "model_dump"): + return value.model_dump(mode="json") + if isinstance(value, Mapping): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonable(item) for item in value] + if value is None or isinstance(value, (bool, int, float, str)): + return value + return repr(value) + + +class _CapturingStream: + """Record stream chunks while preserving the upstream iterator contract.""" + + def __init__(self, stream: Any, chunks: list[Any]) -> None: + self._stream = stream + self._chunks = chunks + + def __iter__(self): + for chunk in self._stream: + self._chunks.append(_jsonable(chunk)) + yield chunk + + +class _CapturingCompletions: + """Delegate OpenAI requests while retaining their raw responses.""" + + def __init__(self, completions: Any, record: dict[str, Any]) -> None: + self._completions = completions + self._record = record + + def create(self, **request: Any) -> Any: + response = self._completions.create(**request) + if request.get("stream"): + chunks: list[Any] = [] + self._record["raw_chunks"] = chunks + return _CapturingStream(response, chunks) + self._record["raw_response"] = _jsonable(response) + return response + + +class _CapturingClient: + """Expose the OpenAI chat interface expected by the upstream helper.""" + + def __init__(self, client: Any, record: dict[str, Any]) -> None: + chat_type = type("_CapturingChat", (), {}) + self.chat = chat_type() + self.chat.completions = _CapturingCompletions(client.chat.completions, record) + + +def run_diagnostic_sequence( + *, + verifier_dir: Path, + base_url: str, + api_key: str, + model: str, + output_dir: Path, +) -> None: + """Run ordered stock-helper requests and preserve raw response evidence.""" + sys.path.insert(0, str(verifier_dir)) + try: + validator = importlib.import_module("tests.tool_call_json_schema.validator") + cases = validator.load_cases( + verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" + ) + selected = validator.select_cases( + cases, + selection="object", + requested_cases=set(), + max_cases=1, + ) + if len(selected) != 1: + raise ValueError( + f"diagnostic expected one selected case, found {len(selected)}" + ) + case, schema, selection_reason = selected[0] + client = validator.make_client(base_url, api_key, 120) + records: list[dict[str, Any]] = [] + try: + for index, mode in enumerate(DIAGNOSTIC_MODES, start=1): + record: dict[str, Any] = {"index": index, "mode": mode} + response = validator.send_tool_schema( + _CapturingClient(client, record), + model, + schema, + 2048, + False, + "none", + stream=mode == "stream", + ) + valid, validation_message = validator.validate_arguments( + schema, response.arguments + ) + record.update( + { + "accepted": response.accepted, + "message": response.message, + "arguments": response.arguments, + "arguments_valid": valid, + "validation_message": validation_message, + "http_status": response.http_status, + "error_type": response.error_type, + } + ) + records.append(record) + finally: + client.close() + + report = { + "model": model, + "base_url": base_url, + "case": { + "suite": case.suite, + "line": case.line, + "selection_reason": selection_reason, + "schema": schema, + }, + "sequence": list(DIAGNOSTIC_MODES), + "results": records, + } + (output_dir / DIAGNOSTIC_REPORT_FILENAME).write_text( + json.dumps(report, indent=2) + "\n", + encoding="utf-8", + ) + finally: + sys.path.pop(0) def prepare_compatibility_path(output_dir: Path) -> Path: @@ -167,6 +307,7 @@ def run_evaluation( model: str, output_dir: Path, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, + diagnostic: bool = False, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -179,6 +320,14 @@ def run_evaluation( try: native_report.unlink(missing_ok=True) + if diagnostic: + run_diagnostic_sequence( + verifier_dir=verifier_dir, + base_url=base_url, + api_key=api_key, + model=model, + output_dir=output_dir, + ) completed = subprocess.run( build_pytest_command( base_url=base_url, @@ -237,6 +386,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS ) parser.add_argument("--integration-error") + parser.add_argument( + "--diagnostic", + action="store_true", + help="Run ordered raw request diagnostics before the unchanged verifier", + ) args = parser.parse_args(argv) if args.integration_error is None: missing = [ @@ -276,6 +430,7 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, output_dir=args.output_dir, timeout_seconds=args.timeout_seconds, + diagnostic=args.diagnostic, ) return 0 if passed else 1 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index e133d3b735..048b23d156 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -236,3 +236,70 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" assert _n_eff(output_dir) == 0 + + +def test_diagnostic_sequence_captures_raw_responses( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + verifier_dir = tmp_path / "verifier" + case_dir = verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" + case_dir.mkdir(parents=True) + output_dir = tmp_path / "output" + output_dir.mkdir() + + class FakeResponse: + accepted = True + message = "tool call returned" + arguments = '{"value": {}}' + http_status = None + error_type = None + + class FakeClient: + def __init__(self) -> None: + self.chat = SimpleNamespace( + completions=SimpleNamespace(create=lambda **kwargs: None) + ) + + def close(self) -> None: + pass + + fake_validator = SimpleNamespace( + load_cases=lambda path: ["case"], + select_cases=lambda cases, **kwargs: [ + ( + SimpleNamespace(suite="TestAdditionalProperties", line=1), + {"type": "object"}, + "object_parameter_schema", + ) + ], + make_client=lambda *args: FakeClient(), + send_tool_schema=lambda client, + model, + schema, + max_tokens, + thinking, + think_mode, + *, + stream: FakeResponse(), + validate_arguments=lambda schema, arguments: (True, "valid"), + ) + monkeypatch.setattr( + kve.importlib, + "import_module", + lambda name: fake_validator, + ) + + kve.run_diagnostic_sequence( + verifier_dir=verifier_dir, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + ) + + report = json.loads((output_dir / kve.DIAGNOSTIC_REPORT_FILENAME).read_text()) + assert report["sequence"] == list(kve.DIAGNOSTIC_MODES) + assert [record["mode"] for record in report["results"]] == list( + kve.DIAGNOSTIC_MODES + ) + assert all(record["arguments_valid"] for record in report["results"]) From 23851fb80ff82729611b32f72cf5432f3d6d092d Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:30:45 -0500 Subject: [PATCH 19/99] Revert "test: capture Kimi response diagnostics" This reverts commit 134906e56d97408b8b457f06e60fce6fc16030ba. --- .../workflows/benchmark-multinode-tmpl.yml | 7 - .github/workflows/e2e-tests.yml | 11 -- benchmarks/benchmark_lib.sh | 5 - utils/evals/kimi_vendor_eval.py | 157 +----------------- utils/evals/test_kimi_vendor_eval.py | 67 -------- 5 files changed, 1 insertion(+), 246 deletions(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 4f2462d137..7004faed99 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -148,11 +148,6 @@ on: type: string required: false default: "" - kimi-vendor-diagnostic: - description: "Capture ordered raw Kimi tool-call responses" - type: boolean - required: false - default: false eval-limit: description: "Eval instance count: empty/full = whole split (default); N = first-N smoke slice" required: false @@ -251,7 +246,6 @@ env: EVAL_SUITE: ${{ inputs.eval-suite }} EVAL_CONC: ${{ inputs.eval-conc }} EVAL_LIMIT: ${{ inputs.eval-limit }} - KIMI_VENDOR_DIAGNOSTIC: ${{ inputs.kimi-vendor-diagnostic && '1' || '0' }} SWEBENCH_GEN_MODE: ${{ inputs.swebench-gen-mode }} # GPU/multi-node runners lack Docker for SWE-bench scoring. SWEBENCH_USE_MODAL: 'true' @@ -490,7 +484,6 @@ jobs: meta_env.json results*.json *_vendor_report.json - eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8d354e6027..a0a31b5e91 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,11 +55,6 @@ on: required: false type: string default: "" - kimi-vendor-diagnostic: - description: "Capture ordered raw Kimi tool-call responses" - required: false - type: boolean - default: false swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -150,11 +145,6 @@ on: required: false type: string default: "" - kimi-vendor-diagnostic: - description: "Capture ordered raw Kimi tool-call responses" - required: false - type: boolean - default: false swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -611,7 +601,6 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} - kimi-vendor-diagnostic: ${{ inputs.kimi-vendor-diagnostic }} scenario-type: agentic-coding ref: ${{ inputs.ref }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 21bae9908e..cef240b274 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1150,10 +1150,6 @@ _run_kimi_tool_call_schema_eval() { fi local eval_rc=0 - local diagnostic_args=() - if [ "${KIMI_VENDOR_DIAGNOSTIC:-0}" = "1" ]; then - diagnostic_args=(--diagnostic) - fi PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ python3 "$adapter_path" \ --verifier-dir "$checkout_dir" \ @@ -1161,7 +1157,6 @@ _run_kimi_tool_call_schema_eval() { --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ - "${diagnostic_args[@]}" \ || eval_rc=$? _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" return "$eval_rc" diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 9f791b318e..4f3debc6fb 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -4,10 +4,9 @@ from __future__ import annotations import argparse -import importlib import json -import sys import subprocess +import sys from collections.abc import Mapping, Sequence from datetime import datetime, timezone from pathlib import Path @@ -20,145 +19,6 @@ DEFAULT_TIMEOUT_SECONDS = 900 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" -DIAGNOSTIC_REPORT_FILENAME = "eval_kimi_tool_call_diagnostic.json" -DIAGNOSTIC_MODES = ( - "non-stream", - "non-stream", - "stream", - "stream", - "stream", - "non-stream", -) - - -def _jsonable(value: Any) -> Any: - """Convert an OpenAI response model into JSON-compatible data.""" - if hasattr(value, "model_dump"): - return value.model_dump(mode="json") - if isinstance(value, Mapping): - return {str(key): _jsonable(item) for key, item in value.items()} - if isinstance(value, (list, tuple)): - return [_jsonable(item) for item in value] - if value is None or isinstance(value, (bool, int, float, str)): - return value - return repr(value) - - -class _CapturingStream: - """Record stream chunks while preserving the upstream iterator contract.""" - - def __init__(self, stream: Any, chunks: list[Any]) -> None: - self._stream = stream - self._chunks = chunks - - def __iter__(self): - for chunk in self._stream: - self._chunks.append(_jsonable(chunk)) - yield chunk - - -class _CapturingCompletions: - """Delegate OpenAI requests while retaining their raw responses.""" - - def __init__(self, completions: Any, record: dict[str, Any]) -> None: - self._completions = completions - self._record = record - - def create(self, **request: Any) -> Any: - response = self._completions.create(**request) - if request.get("stream"): - chunks: list[Any] = [] - self._record["raw_chunks"] = chunks - return _CapturingStream(response, chunks) - self._record["raw_response"] = _jsonable(response) - return response - - -class _CapturingClient: - """Expose the OpenAI chat interface expected by the upstream helper.""" - - def __init__(self, client: Any, record: dict[str, Any]) -> None: - chat_type = type("_CapturingChat", (), {}) - self.chat = chat_type() - self.chat.completions = _CapturingCompletions(client.chat.completions, record) - - -def run_diagnostic_sequence( - *, - verifier_dir: Path, - base_url: str, - api_key: str, - model: str, - output_dir: Path, -) -> None: - """Run ordered stock-helper requests and preserve raw response evidence.""" - sys.path.insert(0, str(verifier_dir)) - try: - validator = importlib.import_module("tests.tool_call_json_schema.validator") - cases = validator.load_cases( - verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" - ) - selected = validator.select_cases( - cases, - selection="object", - requested_cases=set(), - max_cases=1, - ) - if len(selected) != 1: - raise ValueError( - f"diagnostic expected one selected case, found {len(selected)}" - ) - case, schema, selection_reason = selected[0] - client = validator.make_client(base_url, api_key, 120) - records: list[dict[str, Any]] = [] - try: - for index, mode in enumerate(DIAGNOSTIC_MODES, start=1): - record: dict[str, Any] = {"index": index, "mode": mode} - response = validator.send_tool_schema( - _CapturingClient(client, record), - model, - schema, - 2048, - False, - "none", - stream=mode == "stream", - ) - valid, validation_message = validator.validate_arguments( - schema, response.arguments - ) - record.update( - { - "accepted": response.accepted, - "message": response.message, - "arguments": response.arguments, - "arguments_valid": valid, - "validation_message": validation_message, - "http_status": response.http_status, - "error_type": response.error_type, - } - ) - records.append(record) - finally: - client.close() - - report = { - "model": model, - "base_url": base_url, - "case": { - "suite": case.suite, - "line": case.line, - "selection_reason": selection_reason, - "schema": schema, - }, - "sequence": list(DIAGNOSTIC_MODES), - "results": records, - } - (output_dir / DIAGNOSTIC_REPORT_FILENAME).write_text( - json.dumps(report, indent=2) + "\n", - encoding="utf-8", - ) - finally: - sys.path.pop(0) def prepare_compatibility_path(output_dir: Path) -> Path: @@ -307,7 +167,6 @@ def run_evaluation( model: str, output_dir: Path, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, - diagnostic: bool = False, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -320,14 +179,6 @@ def run_evaluation( try: native_report.unlink(missing_ok=True) - if diagnostic: - run_diagnostic_sequence( - verifier_dir=verifier_dir, - base_url=base_url, - api_key=api_key, - model=model, - output_dir=output_dir, - ) completed = subprocess.run( build_pytest_command( base_url=base_url, @@ -386,11 +237,6 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS ) parser.add_argument("--integration-error") - parser.add_argument( - "--diagnostic", - action="store_true", - help="Run ordered raw request diagnostics before the unchanged verifier", - ) args = parser.parse_args(argv) if args.integration_error is None: missing = [ @@ -430,7 +276,6 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, output_dir=args.output_dir, timeout_seconds=args.timeout_seconds, - diagnostic=args.diagnostic, ) return 0 if passed else 1 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index 048b23d156..e133d3b735 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -236,70 +236,3 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" assert _n_eff(output_dir) == 0 - - -def test_diagnostic_sequence_captures_raw_responses( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - verifier_dir = tmp_path / "verifier" - case_dir = verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" - case_dir.mkdir(parents=True) - output_dir = tmp_path / "output" - output_dir.mkdir() - - class FakeResponse: - accepted = True - message = "tool call returned" - arguments = '{"value": {}}' - http_status = None - error_type = None - - class FakeClient: - def __init__(self) -> None: - self.chat = SimpleNamespace( - completions=SimpleNamespace(create=lambda **kwargs: None) - ) - - def close(self) -> None: - pass - - fake_validator = SimpleNamespace( - load_cases=lambda path: ["case"], - select_cases=lambda cases, **kwargs: [ - ( - SimpleNamespace(suite="TestAdditionalProperties", line=1), - {"type": "object"}, - "object_parameter_schema", - ) - ], - make_client=lambda *args: FakeClient(), - send_tool_schema=lambda client, - model, - schema, - max_tokens, - thinking, - think_mode, - *, - stream: FakeResponse(), - validate_arguments=lambda schema, arguments: (True, "valid"), - ) - monkeypatch.setattr( - kve.importlib, - "import_module", - lambda name: fake_validator, - ) - - kve.run_diagnostic_sequence( - verifier_dir=verifier_dir, - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, - ) - - report = json.loads((output_dir / kve.DIAGNOSTIC_REPORT_FILENAME).read_text()) - assert report["sequence"] == list(kve.DIAGNOSTIC_MODES) - assert [record["mode"] for record in report["results"]] == list( - kve.DIAGNOSTIC_MODES - ) - assert all(record["arguments_valid"] for record in report["results"]) From 9a661a60c2394bba27358648614544116d49c7f0 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:50:56 -0500 Subject: [PATCH 20/99] test: capture deterministic Kimi diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:采集确定性的 Kimi 工具调用诊断数据。 --- .../workflows/benchmark-multinode-tmpl.yml | 26 ++ .github/workflows/benchmark-tmpl.yml | 26 ++ .github/workflows/e2e-tests.yml | 60 +++ benchmarks/benchmark_lib.sh | 10 + utils/evals/kimi_vendor_eval.py | 351 +++++++++++++++++- utils/evals/test_kimi_vendor_eval.py | 194 ++++++++++ 6 files changed, 666 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 7004faed99..06a58745d9 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -143,6 +143,26 @@ on: type: string required: false default: "" + kimi-tool-call-diagnostic: + description: "Run temporary ordered Kimi tool-call diagnostics" + type: boolean + required: false + default: false + kimi-tool-call-diagnostic-temperature: + description: "Temperature for temporary Kimi diagnostics" + type: string + required: false + default: "0" + kimi-tool-call-diagnostic-seed: + description: "Seed for temporary Kimi diagnostics" + type: string + required: false + default: "1" + kimi-tool-call-diagnostic-sequence: + description: "Comma-separated unary/stream diagnostic order" + type: string + required: false + default: "unary,unary,stream,stream,stream,unary" eval-conc: description: "Concurrency value or space-separated list for eval requests (overrides default max-of-conc-list)" type: string @@ -244,6 +264,10 @@ env: EVAL_ONLY: ${{ inputs.eval-only }} EVAL_FRAMEWORK: ${{ inputs.eval-framework }} EVAL_SUITE: ${{ inputs.eval-suite }} + KIMI_TOOL_CALL_DIAGNOSTIC: ${{ inputs.kimi-tool-call-diagnostic }} + KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + KIMI_TOOL_CALL_DIAGNOSTIC_SEED: ${{ inputs.kimi-tool-call-diagnostic-seed }} + KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE: ${{ inputs.kimi-tool-call-diagnostic-sequence }} EVAL_CONC: ${{ inputs.eval-conc }} EVAL_LIMIT: ${{ inputs.eval-limit }} SWEBENCH_GEN_MODE: ${{ inputs.swebench-gen-mode }} @@ -484,6 +508,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl @@ -506,6 +531,7 @@ jobs: rm -f meta_env.json || true rm -f results*.json || true rm -f *_vendor_report.json || true + rm -f eval_kimi_tool_call_diagnostic.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 27f933dec5..612bb005f2 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -95,6 +95,26 @@ on: type: string required: false default: "" + kimi-tool-call-diagnostic: + description: "Run temporary ordered Kimi tool-call diagnostics" + type: boolean + required: false + default: false + kimi-tool-call-diagnostic-temperature: + description: "Temperature for temporary Kimi diagnostics" + type: string + required: false + default: "0" + kimi-tool-call-diagnostic-seed: + description: "Seed for temporary Kimi diagnostics" + type: string + required: false + default: "1" + kimi-tool-call-diagnostic-sequence: + description: "Comma-separated unary/stream diagnostic order" + type: string + required: false + default: "unary,unary,stream,stream,stream,unary" random-range-ratio: required: false type: string @@ -185,6 +205,10 @@ env: EVAL_ONLY: ${{ inputs.eval-only }} EVAL_FRAMEWORK: ${{ inputs.eval-framework }} EVAL_SUITE: ${{ inputs.eval-suite }} + KIMI_TOOL_CALL_DIAGNOSTIC: ${{ inputs.kimi-tool-call-diagnostic }} + KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + KIMI_TOOL_CALL_DIAGNOSTIC_SEED: ${{ inputs.kimi-tool-call-diagnostic-seed }} + KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE: ${{ inputs.kimi-tool-call-diagnostic-sequence }} # Agentic-coding env. Fixed-seq-len jobs leave these empty. SCENARIO_TYPE: ${{ inputs.scenario-type }} SCENARIO_SUBDIR: ${{ inputs.scenario-type == 'agentic-coding' && 'agentic/' || 'fixed_seq_len/' }} @@ -405,6 +429,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl @@ -423,6 +448,7 @@ jobs: # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true rm -f -- ./*_vendor_report.json || true + rm -f eval_kimi_tool_call_diagnostic.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a0a31b5e91..2e52a9289e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,6 +55,26 @@ on: required: false type: string default: "" + kimi-tool-call-diagnostic: + description: "Run temporary ordered Kimi tool-call diagnostics" + required: false + type: boolean + default: false + kimi-tool-call-diagnostic-temperature: + description: "Temperature for temporary Kimi diagnostics" + required: false + type: string + default: "0" + kimi-tool-call-diagnostic-seed: + description: "Seed for temporary Kimi diagnostics" + required: false + type: string + default: "1" + kimi-tool-call-diagnostic-sequence: + description: "Comma-separated unary/stream diagnostic order" + required: false + type: string + default: "unary,unary,stream,stream,stream,unary" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -145,6 +165,26 @@ on: required: false type: string default: "" + kimi-tool-call-diagnostic: + description: "Run temporary ordered Kimi tool-call diagnostics" + required: false + type: boolean + default: false + kimi-tool-call-diagnostic-temperature: + description: "Temperature for temporary Kimi diagnostics" + required: false + type: string + default: "0" + kimi-tool-call-diagnostic-seed: + description: "Seed for temporary Kimi diagnostics" + required: false + type: string + default: "1" + kimi-tool-call-diagnostic-sequence: + description: "Comma-separated unary/stream diagnostic order" + required: false + type: string + default: "unary,unary,stream,stream,stream,unary" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -398,6 +438,12 @@ jobs: run-eval: true eval-only: true eval-conc: ${{ matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} + eval-framework: ${{ inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite }} + kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} + kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} + kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} ref: ${{ inputs.ref }} test-sweep-agentic: @@ -484,6 +530,10 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} + kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} + kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} + kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -601,6 +651,10 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} + kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} + kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} + kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -679,6 +733,12 @@ jobs: run-eval: true eval-only: true eval-limit: ${{ inputs.eval-limit }} + eval-framework: ${{ inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite }} + kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} + kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} + kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} + kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} ref: ${{ inputs.ref }} collect-results: diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index cef240b274..e36f840c51 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1113,6 +1113,15 @@ _run_kimi_tool_call_schema_eval() { local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/kimi_vendor_eval.py" local runtime_dir="" local checkout_dir="" + local diagnostic_args=() + if [[ "${KIMI_TOOL_CALL_DIAGNOSTIC:-false}" == "true" ]]; then + diagnostic_args+=(--diagnostic) + diagnostic_args+=( + --diagnostic-temperature "${KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE:-0}" + --diagnostic-seed "${KIMI_TOOL_CALL_DIAGNOSTIC_SEED:-1}" + --diagnostic-sequence "${KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE:-unary,unary,stream,stream,stream,unary}" + ) + fi mkdir -p "$results_dir" || return $? export EVAL_RESULT_DIR="$results_dir" @@ -1157,6 +1166,7 @@ _run_kimi_tool_call_schema_eval() { --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ + "${diagnostic_args[@]}" \ || eval_rc=$? _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" return "$eval_rc" diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 4f3debc6fb..8da0eb9884 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -4,6 +4,8 @@ from __future__ import annotations import argparse +import importlib +import importlib.metadata import json import subprocess import sys @@ -19,6 +21,301 @@ DEFAULT_TIMEOUT_SECONDS = 900 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" +DIAGNOSTIC_REPORT_FILENAME = "eval_kimi_tool_call_diagnostic.json" +DEFAULT_DIAGNOSTIC_TEMPERATURE = 0.0 +DEFAULT_DIAGNOSTIC_SEED = 1 +DEFAULT_DIAGNOSTIC_SEQUENCE = ( + "unary", + "unary", + "stream", + "stream", + "stream", + "unary", +) + + +def _utc_timestamp() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _jsonable(value: Any) -> Any: + """Convert OpenAI response models into JSON-compatible evidence.""" + if hasattr(value, "model_dump"): + return value.model_dump(mode="json") + if isinstance(value, Mapping): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonable(item) for item in value] + if value is None or isinstance(value, (bool, int, float, str)): + return value + return repr(value) + + +def _response_ids(value: Any) -> list[str]: + ids: list[str] = [] + if isinstance(value, Mapping): + response_id = value.get("id") + if isinstance(response_id, str) and response_id not in ids: + ids.append(response_id) + for item in value.values(): + for nested_id in _response_ids(item): + if nested_id not in ids: + ids.append(nested_id) + elif isinstance(value, list): + for item in value: + for nested_id in _response_ids(item): + if nested_id not in ids: + ids.append(nested_id) + return ids + + +class _CapturingStream: + """Record stream chunks while preserving the upstream iterator contract.""" + + def __init__(self, stream: Any, record: dict[str, Any]) -> None: + self._stream = stream + self._record = record + + def __iter__(self): + try: + for chunk in self._stream: + raw_chunk = _jsonable(chunk) + self._record["raw_chunks"].append(raw_chunk) + for response_id in _response_ids(raw_chunk): + if response_id not in self._record["response_ids"]: + self._record["response_ids"].append(response_id) + yield chunk + except Exception as exc: + self._record["transport_error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + raise + + +class _CapturingCompletions: + """Delegate OpenAI requests while retaining exact payloads and raw replies.""" + + def __init__( + self, + completions: Any, + record: dict[str, Any], + request_overrides: Mapping[str, Any], + ) -> None: + self._completions = completions + self._record = record + self._request_overrides = request_overrides + + def create(self, **request: Any) -> Any: + request.update(self._request_overrides) + self._record["request_payload"] = _jsonable(request) + self._record["request_started_at"] = _utc_timestamp() + try: + response = self._completions.create(**request) + except Exception as exc: + self._record["response_received_at"] = _utc_timestamp() + self._record["transport_error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + raise + self._record["response_received_at"] = _utc_timestamp() + if request.get("stream"): + self._record["raw_chunks"] = [] + return _CapturingStream(response, self._record) + raw_response = _jsonable(response) + self._record["raw_response"] = raw_response + self._record["response_ids"] = _response_ids(raw_response) + return response + + +class _CapturingClient: + """Expose the OpenAI chat interface expected by the upstream helper.""" + + def __init__( + self, + client: Any, + record: dict[str, Any], + request_overrides: Mapping[str, Any], + ) -> None: + chat_type = type("_CapturingChat", (), {}) + self.chat = chat_type() + self.chat.completions = _CapturingCompletions( + client.chat.completions, record, request_overrides + ) + + +def _runtime_versions() -> dict[str, str]: + versions: dict[str, str] = {"python": sys.version} + for distribution in ("openai", "httpx", "jsonschema"): + try: + versions[distribution] = importlib.metadata.version(distribution) + except importlib.metadata.PackageNotFoundError: + versions[distribution] = "not-installed" + return versions + +def _capture_version_endpoint(base_url: str) -> dict[str, Any]: + version_url = f"{base_url.removesuffix('/v1').rstrip('/')}/version" + started_at = _utc_timestamp() + try: + httpx = importlib.import_module("httpx") + response = httpx.get(version_url, timeout=5.0) + safe_headers = { + key: value + for key, value in response.headers.items() + if key.lower() + in { + "content-type", + "date", + "server", + "x-request-id", + "x-sglang-version", + } + } + return { + "url": version_url, + "started_at": started_at, + "completed_at": _utc_timestamp(), + "status_code": response.status_code, + "headers": safe_headers, + "body": response.text, + } + except Exception as exc: + return { + "url": version_url, + "started_at": started_at, + "completed_at": _utc_timestamp(), + "error": {"type": type(exc).__name__, "message": str(exc)}, + } + + +def run_diagnostic_sequence( + *, + verifier_dir: Path, + base_url: str, + api_key: str, + model: str, + output_dir: Path, + temperature: float = DEFAULT_DIAGNOSTIC_TEMPERATURE, + seed: int = DEFAULT_DIAGNOSTIC_SEED, + sequence: Sequence[str] = DEFAULT_DIAGNOSTIC_SEQUENCE, +) -> None: + """Run ordered stock-helper requests and preserve client-visible evidence.""" + report_path = output_dir / DIAGNOSTIC_REPORT_FILENAME + records: list[dict[str, Any]] = [] + report: dict[str, Any] = { + "model": model, + "base_url": base_url, + "started_at": _utc_timestamp(), + "controls": { + "temperature": temperature, + "seed": seed, + "sequence": list(sequence), + }, + "runtime_versions": _runtime_versions(), + "version_endpoint": _capture_version_endpoint(base_url), + "results": records, + } + sys.path.insert(0, str(verifier_dir)) + client: Any = None + try: + validator = importlib.import_module("tests.tool_call_json_schema.validator") + report["parser"] = { + "module": validator.__name__, + "file": str(Path(validator.__file__).resolve()), + } + cases = validator.load_cases( + verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" + ) + selected = validator.select_cases( + cases, + selection="object", + requested_cases=set(), + max_cases=1, + ) + if len(selected) != 1: + raise ValueError( + f"diagnostic expected one selected case, found {len(selected)}" + ) + case, schema, selection_reason = selected[0] + report["case"] = { + "suite": case.suite, + "line": case.line, + "selection_reason": selection_reason, + "schema": schema, + } + client = validator.make_client(base_url, api_key, 120) + mode_occurrences = {"unary": 0, "stream": 0} + for index, mode in enumerate(sequence, start=1): + mode_occurrences[mode] += 1 + record: dict[str, Any] = { + "index": index, + "mode": mode, + "mode_occurrence": mode_occurrences[mode], + "temperature_state": ( + "cold" if mode_occurrences[mode] == 1 else "warm" + ), + "sampling_controls": { + "temperature": temperature, + "seed": seed, + }, + "started_at": _utc_timestamp(), + "response_ids": [], + } + records.append(record) + try: + response = validator.send_tool_schema( + _CapturingClient( + client, + record, + {"temperature": temperature, "seed": seed}, + ), + model, + schema, + 2048, + False, + "none", + stream=mode == "stream", + ) + valid, validation_message = validator.validate_arguments( + schema, response.arguments + ) + record["parser_output"] = { + "accepted": response.accepted, + "message": response.message, + "arguments": response.arguments, + "arguments_valid": valid, + "validation_message": validation_message, + "http_status": response.http_status, + "error_type": response.error_type, + } + except Exception as exc: + record["diagnostic_error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + finally: + record["completed_at"] = _utc_timestamp() + except Exception as exc: + report["setup_error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + finally: + if client is not None: + try: + client.close() + except Exception as exc: + report["client_close_error"] = { + "type": type(exc).__name__, + "message": str(exc), + } + report["completed_at"] = _utc_timestamp() + report_path.write_text( + json.dumps(report, indent=2) + "\n", + encoding="utf-8", + ) + sys.path.pop(0) def prepare_compatibility_path(output_dir: Path) -> Path: @@ -167,6 +464,10 @@ def run_evaluation( model: str, output_dir: Path, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, + diagnostic: bool = False, + diagnostic_temperature: float = DEFAULT_DIAGNOSTIC_TEMPERATURE, + diagnostic_seed: int = DEFAULT_DIAGNOSTIC_SEED, + diagnostic_sequence: Sequence[str] = DEFAULT_DIAGNOSTIC_SEQUENCE, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -176,9 +477,25 @@ def run_evaluation( integration_error: BaseException | None = None compatibility = _compatibility_result(model, 0.0, n_samples=0) complete_pass = False - try: native_report.unlink(missing_ok=True) + if diagnostic: + try: + run_diagnostic_sequence( + verifier_dir=verifier_dir, + base_url=base_url, + api_key=api_key, + model=model, + output_dir=output_dir, + temperature=diagnostic_temperature, + seed=diagnostic_seed, + sequence=diagnostic_sequence, + ) + except Exception as exc: + print( + f"WARNING: Kimi diagnostic collection failed: {exc}", + file=sys.stderr, + ) completed = subprocess.run( build_pytest_command( base_url=base_url, @@ -223,6 +540,14 @@ def _positive_int(value: str) -> int: raise argparse.ArgumentTypeError("must be a positive integer") return parsed +def _diagnostic_sequence(value: str) -> tuple[str, ...]: + sequence = tuple(item.strip() for item in value.split(",") if item.strip()) + if not sequence or any(mode not in {"unary", "stream"} for mode in sequence): + raise argparse.ArgumentTypeError( + "must be a comma-separated sequence of unary and stream" + ) + return sequence + def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( @@ -237,6 +562,26 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS ) parser.add_argument("--integration-error") + parser.add_argument( + "--diagnostic", + action="store_true", + help="Run ordered raw request diagnostics before the unchanged verifier", + ) + parser.add_argument( + "--diagnostic-temperature", + type=float, + default=DEFAULT_DIAGNOSTIC_TEMPERATURE, + ) + parser.add_argument( + "--diagnostic-seed", + type=int, + default=DEFAULT_DIAGNOSTIC_SEED, + ) + parser.add_argument( + "--diagnostic-sequence", + type=_diagnostic_sequence, + default=DEFAULT_DIAGNOSTIC_SEQUENCE, + ) args = parser.parse_args(argv) if args.integration_error is None: missing = [ @@ -276,6 +621,10 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, output_dir=args.output_dir, timeout_seconds=args.timeout_seconds, + diagnostic=args.diagnostic, + diagnostic_temperature=args.diagnostic_temperature, + diagnostic_seed=args.diagnostic_seed, + diagnostic_sequence=args.diagnostic_sequence, ) return 0 if passed else 1 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index e133d3b735..85aef1e9c6 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -236,3 +236,197 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" assert _n_eff(output_dir) == 0 + + +def test_diagnostic_defaults_are_explicit_and_stock_command_is_unchanged( + tmp_path: Path, +) -> None: + args = kve.parse_args( + [ + "--verifier-dir", + str(tmp_path), + "--base-url", + "http://localhost/v1", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--diagnostic", + ] + ) + assert args.diagnostic_temperature == 0 + assert args.diagnostic_seed == 1 + assert args.diagnostic_sequence == ( + "unary", + "unary", + "stream", + "stream", + "stream", + "unary", + ) + assert "--diagnostic" not in kve.build_pytest_command( + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + report_path=tmp_path / "report.json", + ) + + +def test_diagnostic_captures_payload_sequence_and_raw_modes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + verifier_dir = tmp_path / "verifier" + (verifier_dir / "testdata/walle_validator_cases/validator_cases").mkdir( + parents=True + ) + output_dir = tmp_path / "output" + output_dir.mkdir() + requests: list[dict[str, Any]] = [] + + class Raw: + def __init__(self, value: dict[str, Any]) -> None: + self.value = value + + def model_dump(self, *, mode: str) -> dict[str, Any]: + assert mode == "json" + return self.value + + class Completions: + def create(self, **request: Any) -> Any: + requests.append(request) + raw = { + "id": f"response-{len(requests)}", + "choices": [ + { + "message": { + "reasoning_content": "reasoning", + "tool_calls": [], + } + } + ], + } + if request.get("stream"): + return iter([Raw(raw)]) + return Raw(raw) + + client = SimpleNamespace( + chat=SimpleNamespace(completions=Completions()), + close=lambda: None, + ) + + def send_tool_schema( + capturing_client: Any, + model: str, + schema: Any, + max_tokens: int, + thinking: bool, + think_mode: str, + *, + stream: bool, + ) -> SimpleNamespace: + response = capturing_client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": "identical"}], + tools=[{"type": "function", "function": {"parameters": schema}}], + max_tokens=max_tokens, + stream=stream, + ) + if stream: + list(response) + return SimpleNamespace( + accepted=True, + message="parsed", + arguments="{}", + http_status=None, + error_type=None, + ) + + fake_validator = SimpleNamespace( + __name__="tests.tool_call_json_schema.validator", + __file__=str(verifier_dir / "validator.py"), + load_cases=lambda path: ["case"], + select_cases=lambda cases, **kwargs: [ + ( + SimpleNamespace(suite="suite", line=1), + {"type": "object"}, + "object_parameter_schema", + ) + ], + make_client=lambda *args: client, + send_tool_schema=send_tool_schema, + validate_arguments=lambda schema, arguments: (True, "valid"), + ) + monkeypatch.setattr(kve.importlib, "import_module", lambda name: fake_validator) + monkeypatch.setattr( + kve, + "_capture_version_endpoint", + lambda base_url: {"status_code": 200, "body": "v1"}, + ) + + kve.run_diagnostic_sequence( + verifier_dir=verifier_dir, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + temperature=0.25, + seed=7, + sequence=("unary", "stream", "unary"), + ) + + report = json.loads((output_dir / kve.DIAGNOSTIC_REPORT_FILENAME).read_text()) + assert report["controls"] == { + "temperature": 0.25, + "seed": 7, + "sequence": ["unary", "stream", "unary"], + } + assert [row["mode"] for row in report["results"]] == [ + "unary", + "stream", + "unary", + ] + assert [row["temperature_state"] for row in report["results"]] == [ + "cold", + "cold", + "warm", + ] + assert all( + request["temperature"] == 0.25 and request["seed"] == 7 + for request in requests + ) + assert report["results"][0]["request_payload"] == requests[0] + assert report["results"][0]["raw_response"]["choices"][0]["message"][ + "reasoning_content" + ] == "reasoning" + assert report["results"][1]["raw_chunks"][0]["id"] == "response-2" + assert report["results"][1]["response_ids"] == ["response-2"] + assert report["results"][2]["parser_output"]["arguments_valid"] is True + + +def test_diagnostic_failure_does_not_change_stock_score( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + output_dir = tmp_path / "output" + + def broken_diagnostic(**kwargs: Any) -> None: + raise RuntimeError("diagnostic failed") + + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + Path(command[command.index("--tool-json-report") + 1]).write_text( + json.dumps(_report()) + ) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(kve, "run_diagnostic_sequence", broken_diagnostic) + monkeypatch.setattr(kve.subprocess, "run", fake_run) + assert kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + diagnostic=True, + ) + assert _score(output_dir) == 1.0 \ No newline at end of file From 53b3ca822ba78d23ac38e3e94cf7a15cfea817bc Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:20:33 -0500 Subject: [PATCH 21/99] fix: enable Kimi structural tool constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:启用 Kimi 结构化工具调用约束。 --- .../agg-gb200-dep16-throughput-agentic.yaml | 1 + ...throughput-vllm-simple-offload-agentic.yaml | 1 + .../agg-gb200-tep16-balanced-agentic.yaml | 1 + .../agg-gb200-tp16-latency-agentic.yaml | 1 + runners/test_slurm_utils.py | 18 ++++++++++++++++++ 5 files changed, 22 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index e7de6389a1..3ab5688e08 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -116,6 +116,7 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" + dyn-enable-structural-tag: true reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index 703262fc3b..d80a3d1e49 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -119,6 +119,7 @@ backend: kv-transfer-config: '{"kv_connector":"SimpleCPUOffloadConnector","kv_role":"kv_both","kv_connector_extra_config":{"cpu_bytes_to_use":549755813888,"cpu_bytes_to_use_per_rank":137438953472,"lazy_offload":false}}' scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" + dyn-enable-structural-tag: true reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index e22dae3f38..6c73e65705 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -108,6 +108,7 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" + dyn-enable-structural-tag: true reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 2566aa62f4..bb1fc550e5 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -105,6 +105,7 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" + dyn-enable-structural-tag: true reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index bfce571816..21dcd7e654 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -242,6 +242,24 @@ def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: assert compilation_config["pass_config"]["fuse_allreduce_rms"] is False + +def test_gb200_dynamo_kimi_recipes_enable_structural_tool_constraints() -> None: + recipe_dir = ( + REPO_ROOT / "benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" + ) + recipes = [ + yaml.safe_load(path.read_text()) + for path in recipe_dir.glob("agg-gb200-*-agentic.yaml") + ] + + assert recipes + for recipe in recipes: + frontend = recipe["frontend"] + assert frontend["type"] == "dynamo" + config = recipe["backend"]["vllm_config"]["aggregated"] + assert config["dyn-tool-call-parser"] == "kimi_k3" + assert config["dyn-enable-structural-tag"] is True + def test_b200_kimi_recipe_uses_available_roce_devices() -> None: recipe_path = ( REPO_ROOT From 6d08f1e70d8ceab1d387958ede1ba244dddd3fee Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:26:17 -0500 Subject: [PATCH 22/99] chore: format Kimi recipe regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:格式化 Kimi 配方回归测试。 --- runners/test_slurm_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 21dcd7e654..a89a3354bb 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -2,8 +2,8 @@ import os import subprocess from pathlib import Path -import yaml +import yaml REPO_ROOT = Path(__file__).resolve().parents[1] SLURM_UTILS = REPO_ROOT / "runners" / "slurm_utils.sh" @@ -242,7 +242,6 @@ def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: assert compilation_config["pass_config"]["fuse_allreduce_rms"] is False - def test_gb200_dynamo_kimi_recipes_enable_structural_tool_constraints() -> None: recipe_dir = ( REPO_ROOT / "benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" @@ -260,6 +259,7 @@ def test_gb200_dynamo_kimi_recipes_enable_structural_tool_constraints() -> None: assert config["dyn-tool-call-parser"] == "kimi_k3" assert config["dyn-enable-structural-tag"] is True + def test_b200_kimi_recipe_uses_available_roce_devices() -> None: recipe_path = ( REPO_ROOT From f1fb29da37dfe8883b721de555033ea035a2700c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:11:22 -0500 Subject: [PATCH 23/99] fix: retry transient Kimi verifier downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:重试 Kimi 验证器的临时下载失败。 --- benchmarks/benchmark_lib.sh | 75 +++++++++++++++++++-------- utils/evals/test_run_eval_dispatch.py | 35 ++++++++++++- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index e36f840c51..dd89c18fb5 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -871,6 +871,7 @@ import sys import tarfile import tempfile import time +from urllib.error import HTTPError, URLError from urllib.parse import quote, urlsplit, urlunsplit from urllib.request import Request, urlopen @@ -916,28 +917,58 @@ try: ) with tempfile.TemporaryFile() as archive_file: downloaded = 0 - deadline = time.monotonic() + 60 - with urlopen(request, timeout=60) as response: - while True: - remaining = deadline - time.monotonic() - if remaining <= 0: - raise TimeoutError("archive download exceeded the 60-second deadline") - sock = getattr(getattr(response, "fp", None), "raw", None) - sock = getattr(sock, "_sock", None) - if sock is not None: - sock.settimeout(max(0.001, remaining)) - try: - chunk = response.read(1024 * 1024) - except socket.timeout as error: - raise TimeoutError( - "archive download exceeded the 60-second deadline" - ) from error - if not chunk: - break - downloaded += len(chunk) - if downloaded > 128 * 1024 * 1024: - raise ValueError("archive download exceeds the 128 MiB safety limit") - archive_file.write(chunk) + for attempt in range(1, 4): + archive_file.seek(0) + archive_file.truncate() + downloaded = 0 + deadline = time.monotonic() + 60 + try: + with urlopen(request, timeout=60) as response: + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + "archive download exceeded the 60-second deadline" + ) + sock = getattr(getattr(response, "fp", None), "raw", None) + sock = getattr(sock, "_sock", None) + if sock is not None: + sock.settimeout(max(0.001, remaining)) + try: + chunk = response.read(1024 * 1024) + except socket.timeout as error: + raise TimeoutError( + "archive download exceeded the 60-second deadline" + ) from error + if not chunk: + break + downloaded += len(chunk) + if downloaded > 128 * 1024 * 1024: + raise ValueError( + "archive download exceeds the 128 MiB safety limit" + ) + archive_file.write(chunk) + break + except HTTPError as error: + if error.code not in (408, 429) and not 500 <= error.code < 600: + raise + if attempt == 3: + raise + print( + f"WARN: Kimi-Vendor-Verifier archive download attempt " + f"{attempt}/3 failed: {error}; retrying", + file=sys.stderr, + ) + time.sleep(attempt) + except (TimeoutError, URLError, ConnectionError) as error: + if attempt == 3: + raise + print( + f"WARN: Kimi-Vendor-Verifier archive download attempt " + f"{attempt}/3 failed: {error}; retrying", + file=sys.stderr, + ) + time.sleep(attempt) if downloaded == 0: raise ValueError("downloaded archive is empty") archive_file.seek(0) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 6344ee9247..1dfafca2db 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -380,11 +380,19 @@ def _kimi_verifier_archive( @contextmanager -def _serve_archive(payload: bytes): +def _serve_archive(payload: bytes, *, transient_failures: int = 0): request_paths = [] + request_count = 0 + class ArchiveHandler(BaseHTTPRequestHandler): def do_GET(self): + nonlocal request_count request_paths.append(self.path) + request_count += 1 + if request_count <= transient_failures: + self.send_response(503) + self.end_headers() + return self.send_response(200) self.send_header("Content-Length", str(len(payload))) self.end_headers() @@ -411,6 +419,7 @@ def _prepare_local_kimi_verifier( tmp_path: Path, payload: bytes, verifier_ref: str = "1" * 40, + transient_failures: int = 0, ) -> tuple[subprocess.CompletedProcess[str], Path, list[str]]: checkout = tmp_path / "checkout" script = r''' @@ -419,7 +428,10 @@ def _prepare_local_kimi_verifier( mktemp() { mkdir "$CHECKOUT"; printf '%s\n' "$CHECKOUT"; } _prepare_kimi_vendor_verifier "$REPO_URL" "$VERIFIER_REF" ''' - with _serve_archive(payload) as (repo_url, request_paths): + with _serve_archive( + payload, + transient_failures=transient_failures, + ) as (repo_url, request_paths): result = subprocess.run( ["bash", "-c", script], env={ @@ -455,6 +467,25 @@ def test_kimi_vendor_verifier_fetches_expected_subset_without_git(tmp_path: Path assert "git must not be invoked" not in result.stderr +def test_kimi_vendor_verifier_retries_transient_archive_failure( + tmp_path: Path, +) -> None: + result, checkout, request_paths = _prepare_local_kimi_verifier( + tmp_path, + _kimi_verifier_archive(), + transient_failures=1, + ) + verifier_ref = "1" * 40 + + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == str(checkout) + assert request_paths == [ + f"/owner/verifier/archive/{verifier_ref}.tar.gz", + f"/owner/verifier/archive/{verifier_ref}.tar.gz", + ] + assert "archive download attempt 1/3 failed" in result.stderr + + def test_kimi_vendor_verifier_removes_partial_checkout_when_member_missing( tmp_path: Path, ) -> None: From 0e679c50077d1b011702a732ddc688852fcf5928 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:34:21 -0500 Subject: [PATCH 24/99] fix: stabilize and clean Kimi verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:稳定并清理 Kimi 验证器集成。 --- .../workflows/benchmark-multinode-tmpl.yml | 26 -- .github/workflows/benchmark-tmpl.yml | 26 -- .github/workflows/e2e-tests.yml | 56 --- benchmarks/benchmark_lib.sh | 20 +- utils/evals/EVALS.md | 27 +- utils/evals/kimi_vendor_eval.py | 353 +----------------- utils/evals/test_kimi_vendor_eval.py | 198 +--------- utils/evals/test_run_eval_dispatch.py | 33 +- utils/test_collect_eval_results.py | 28 +- 9 files changed, 80 insertions(+), 687 deletions(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 06a58745d9..7004faed99 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -143,26 +143,6 @@ on: type: string required: false default: "" - kimi-tool-call-diagnostic: - description: "Run temporary ordered Kimi tool-call diagnostics" - type: boolean - required: false - default: false - kimi-tool-call-diagnostic-temperature: - description: "Temperature for temporary Kimi diagnostics" - type: string - required: false - default: "0" - kimi-tool-call-diagnostic-seed: - description: "Seed for temporary Kimi diagnostics" - type: string - required: false - default: "1" - kimi-tool-call-diagnostic-sequence: - description: "Comma-separated unary/stream diagnostic order" - type: string - required: false - default: "unary,unary,stream,stream,stream,unary" eval-conc: description: "Concurrency value or space-separated list for eval requests (overrides default max-of-conc-list)" type: string @@ -264,10 +244,6 @@ env: EVAL_ONLY: ${{ inputs.eval-only }} EVAL_FRAMEWORK: ${{ inputs.eval-framework }} EVAL_SUITE: ${{ inputs.eval-suite }} - KIMI_TOOL_CALL_DIAGNOSTIC: ${{ inputs.kimi-tool-call-diagnostic }} - KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - KIMI_TOOL_CALL_DIAGNOSTIC_SEED: ${{ inputs.kimi-tool-call-diagnostic-seed }} - KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE: ${{ inputs.kimi-tool-call-diagnostic-sequence }} EVAL_CONC: ${{ inputs.eval-conc }} EVAL_LIMIT: ${{ inputs.eval-limit }} SWEBENCH_GEN_MODE: ${{ inputs.swebench-gen-mode }} @@ -508,7 +484,6 @@ jobs: meta_env.json results*.json *_vendor_report.json - eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl @@ -531,7 +506,6 @@ jobs: rm -f meta_env.json || true rm -f results*.json || true rm -f *_vendor_report.json || true - rm -f eval_kimi_tool_call_diagnostic.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 612bb005f2..27f933dec5 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -95,26 +95,6 @@ on: type: string required: false default: "" - kimi-tool-call-diagnostic: - description: "Run temporary ordered Kimi tool-call diagnostics" - type: boolean - required: false - default: false - kimi-tool-call-diagnostic-temperature: - description: "Temperature for temporary Kimi diagnostics" - type: string - required: false - default: "0" - kimi-tool-call-diagnostic-seed: - description: "Seed for temporary Kimi diagnostics" - type: string - required: false - default: "1" - kimi-tool-call-diagnostic-sequence: - description: "Comma-separated unary/stream diagnostic order" - type: string - required: false - default: "unary,unary,stream,stream,stream,unary" random-range-ratio: required: false type: string @@ -205,10 +185,6 @@ env: EVAL_ONLY: ${{ inputs.eval-only }} EVAL_FRAMEWORK: ${{ inputs.eval-framework }} EVAL_SUITE: ${{ inputs.eval-suite }} - KIMI_TOOL_CALL_DIAGNOSTIC: ${{ inputs.kimi-tool-call-diagnostic }} - KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - KIMI_TOOL_CALL_DIAGNOSTIC_SEED: ${{ inputs.kimi-tool-call-diagnostic-seed }} - KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE: ${{ inputs.kimi-tool-call-diagnostic-sequence }} # Agentic-coding env. Fixed-seq-len jobs leave these empty. SCENARIO_TYPE: ${{ inputs.scenario-type }} SCENARIO_SUBDIR: ${{ inputs.scenario-type == 'agentic-coding' && 'agentic/' || 'fixed_seq_len/' }} @@ -429,7 +405,6 @@ jobs: meta_env.json results*.json *_vendor_report.json - eval_kimi_tool_call_diagnostic.json sample*.jsonl agent_preds.json predictions.jsonl @@ -448,7 +423,6 @@ jobs: # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true rm -f -- ./*_vendor_report.json || true - rm -f eval_kimi_tool_call_diagnostic.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 2e52a9289e..dc4bbb665a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,26 +55,6 @@ on: required: false type: string default: "" - kimi-tool-call-diagnostic: - description: "Run temporary ordered Kimi tool-call diagnostics" - required: false - type: boolean - default: false - kimi-tool-call-diagnostic-temperature: - description: "Temperature for temporary Kimi diagnostics" - required: false - type: string - default: "0" - kimi-tool-call-diagnostic-seed: - description: "Seed for temporary Kimi diagnostics" - required: false - type: string - default: "1" - kimi-tool-call-diagnostic-sequence: - description: "Comma-separated unary/stream diagnostic order" - required: false - type: string - default: "unary,unary,stream,stream,stream,unary" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -165,26 +145,6 @@ on: required: false type: string default: "" - kimi-tool-call-diagnostic: - description: "Run temporary ordered Kimi tool-call diagnostics" - required: false - type: boolean - default: false - kimi-tool-call-diagnostic-temperature: - description: "Temperature for temporary Kimi diagnostics" - required: false - type: string - default: "0" - kimi-tool-call-diagnostic-seed: - description: "Seed for temporary Kimi diagnostics" - required: false - type: string - default: "1" - kimi-tool-call-diagnostic-sequence: - description: "Comma-separated unary/stream diagnostic order" - required: false - type: string - default: "unary,unary,stream,stream,stream,unary" swebench-gen-mode: description: "SWE-bench generation mode (single-shot | agentic). Empty = agentic (single-shot is an explicit debugging escape hatch)." required: false @@ -440,10 +400,6 @@ jobs: eval-conc: ${{ matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} - kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} - kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} - kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} ref: ${{ inputs.ref }} test-sweep-agentic: @@ -530,10 +486,6 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} - kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} - kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} - kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -651,10 +603,6 @@ jobs: swebench-gen-mode: ${{ inputs.swebench-gen-mode }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} - kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} - kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} - kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -735,10 +683,6 @@ jobs: eval-limit: ${{ inputs.eval-limit }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} - kimi-tool-call-diagnostic: ${{ inputs.kimi-tool-call-diagnostic }} - kimi-tool-call-diagnostic-temperature: ${{ inputs.kimi-tool-call-diagnostic-temperature }} - kimi-tool-call-diagnostic-seed: ${{ inputs.kimi-tool-call-diagnostic-seed }} - kimi-tool-call-diagnostic-sequence: ${{ inputs.kimi-tool-call-diagnostic-sequence }} ref: ${{ inputs.ref }} collect-results: diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index dd89c18fb5..ecad29da71 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -839,7 +839,8 @@ _install_kimi_vendor_eval_deps() { "httpx[http2]==0.28.1" \ "openai==2.14.0" \ "jsonschema==4.25.1" \ - "pytest==8.4.2" + "pytest==8.4.2" \ + "pytest-rerunfailures==16.4" } _prepare_kimi_vendor_runtime() { @@ -1144,15 +1145,6 @@ _run_kimi_tool_call_schema_eval() { local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/kimi_vendor_eval.py" local runtime_dir="" local checkout_dir="" - local diagnostic_args=() - if [[ "${KIMI_TOOL_CALL_DIAGNOSTIC:-false}" == "true" ]]; then - diagnostic_args+=(--diagnostic) - diagnostic_args+=( - --diagnostic-temperature "${KIMI_TOOL_CALL_DIAGNOSTIC_TEMPERATURE:-0}" - --diagnostic-seed "${KIMI_TOOL_CALL_DIAGNOSTIC_SEED:-1}" - --diagnostic-sequence "${KIMI_TOOL_CALL_DIAGNOSTIC_SEQUENCE:-unary,unary,stream,stream,stream,unary}" - ) - fi mkdir -p "$results_dir" || return $? export EVAL_RESULT_DIR="$results_dir" @@ -1197,7 +1189,6 @@ _run_kimi_tool_call_schema_eval() { --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ - "${diagnostic_args[@]}" \ || eval_rc=$? _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" return "$eval_rc" @@ -1547,7 +1538,7 @@ _write_lm_eval_meta_json() { fi fi fi - local eval_suite="${EVAL_SUITE:-}" + local eval_suite="${EVAL_COMPLETED_SUITE:-${EVAL_SUITE:-}}" if [ -z "$eval_suite" ] && [ -n "${EVAL_TASKS_DIR:-}" ]; then eval_suite="$(basename "${EVAL_TASKS_DIR}")" eval_suite="${eval_suite%.yaml}" @@ -2004,6 +1995,7 @@ run_eval() { local forwarded=() # Keep runner-selected suite identity scoped to this invocation. local EVAL_SUITE="${EVAL_SUITE:-}" + unset EVAL_COMPLETED_SUITE while [[ $# -gt 0 ]]; do case "$1" in @@ -2123,6 +2115,10 @@ run_eval() { *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac + if [ "$framework" = "kimi-vendor" ]; then + export EVAL_COMPLETED_SUITE="$EVAL_SUITE" + fi + # Agentic eval-only recipes have no separate staging step. if [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; then append_lm_eval_summary || true diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 74c4974c1c..9c1375e7c0 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -78,25 +78,32 @@ The smoke runs the unmodified at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run downloads the fresh pinned GitHub source archive and safely extracts only the upstream pytest configuration, tool-call schema tests, and bundled Walle cases. InferenceX does -not install the verifier package or reimplement its request, streaming, retry, -or validation logic. +not install the verifier package or reimplement its request, streaming, or +validation logic. Python 3.12 or newer is required. The runner installs the minimal pinned runtime -(`httpx[http2]`, `openai`, `jsonschema`, and `pytest`) into a temporary isolated -package directory, then runs upstream +(`httpx[http2]`, `openai`, `jsonschema`, `pytest`, and +`pytest-rerunfailures`) into a temporary isolated package directory, then runs +upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; - `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; +- the upstream-recommended `--reruns 3 --reruns-delay 2`; - the bundled Walle case directory and `--tool-json-report`. The selection is `TestAdditionalProperties:1`, parametrized upstream in -non-streaming and streaming modes. The unchanged native report is uploaded as -`kimi_vendor_report.json`. `utils/evals/kimi_vendor_eval.py` only projects its -two outcomes into the existing eval result shape. Both must pass, so the -`kimi_tool_call_schema` threshold is `1.0`. Setup, timeout, and collection -failures emit a zero-score result with error metadata. The adapter bounds the -upstream pytest process to 900 seconds. +non-streaming and streaming modes. Pytest makes one initial attempt and up to +three reruns of each failing mode, with a two-second delay before each rerun. +These retries reduce transient transport and model-sampling flakes; they do not +make the smoke deterministic. The unchanged native report remains one final +outcome per mode because the upstream report deduplicates rerun records by case +and mode. It is uploaded as `kimi_vendor_report.json`, and +`utils/evals/kimi_vendor_eval.py` projects those two outcomes into the existing +eval result shape. Both must pass, so the `kimi_tool_call_schema` threshold is +`1.0`. Setup, timeout, and collection failures emit a zero-score result with +error metadata. The adapter's 900-second global timeout bounds the entire +upstream pytest process, including all attempts and rerun delays. This smoke validates one object-schema tool call. It does not cover tool choice, parallel calls, multi-turn execution, or general agent quality. Multi-value diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 8da0eb9884..8851f2be66 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -4,8 +4,6 @@ from __future__ import annotations import argparse -import importlib -import importlib.metadata import json import subprocess import sys @@ -21,301 +19,6 @@ DEFAULT_TIMEOUT_SECONDS = 900 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" -DIAGNOSTIC_REPORT_FILENAME = "eval_kimi_tool_call_diagnostic.json" -DEFAULT_DIAGNOSTIC_TEMPERATURE = 0.0 -DEFAULT_DIAGNOSTIC_SEED = 1 -DEFAULT_DIAGNOSTIC_SEQUENCE = ( - "unary", - "unary", - "stream", - "stream", - "stream", - "unary", -) - - -def _utc_timestamp() -> str: - return datetime.now(timezone.utc).isoformat() - - -def _jsonable(value: Any) -> Any: - """Convert OpenAI response models into JSON-compatible evidence.""" - if hasattr(value, "model_dump"): - return value.model_dump(mode="json") - if isinstance(value, Mapping): - return {str(key): _jsonable(item) for key, item in value.items()} - if isinstance(value, (list, tuple)): - return [_jsonable(item) for item in value] - if value is None or isinstance(value, (bool, int, float, str)): - return value - return repr(value) - - -def _response_ids(value: Any) -> list[str]: - ids: list[str] = [] - if isinstance(value, Mapping): - response_id = value.get("id") - if isinstance(response_id, str) and response_id not in ids: - ids.append(response_id) - for item in value.values(): - for nested_id in _response_ids(item): - if nested_id not in ids: - ids.append(nested_id) - elif isinstance(value, list): - for item in value: - for nested_id in _response_ids(item): - if nested_id not in ids: - ids.append(nested_id) - return ids - - -class _CapturingStream: - """Record stream chunks while preserving the upstream iterator contract.""" - - def __init__(self, stream: Any, record: dict[str, Any]) -> None: - self._stream = stream - self._record = record - - def __iter__(self): - try: - for chunk in self._stream: - raw_chunk = _jsonable(chunk) - self._record["raw_chunks"].append(raw_chunk) - for response_id in _response_ids(raw_chunk): - if response_id not in self._record["response_ids"]: - self._record["response_ids"].append(response_id) - yield chunk - except Exception as exc: - self._record["transport_error"] = { - "type": type(exc).__name__, - "message": str(exc), - } - raise - - -class _CapturingCompletions: - """Delegate OpenAI requests while retaining exact payloads and raw replies.""" - - def __init__( - self, - completions: Any, - record: dict[str, Any], - request_overrides: Mapping[str, Any], - ) -> None: - self._completions = completions - self._record = record - self._request_overrides = request_overrides - - def create(self, **request: Any) -> Any: - request.update(self._request_overrides) - self._record["request_payload"] = _jsonable(request) - self._record["request_started_at"] = _utc_timestamp() - try: - response = self._completions.create(**request) - except Exception as exc: - self._record["response_received_at"] = _utc_timestamp() - self._record["transport_error"] = { - "type": type(exc).__name__, - "message": str(exc), - } - raise - self._record["response_received_at"] = _utc_timestamp() - if request.get("stream"): - self._record["raw_chunks"] = [] - return _CapturingStream(response, self._record) - raw_response = _jsonable(response) - self._record["raw_response"] = raw_response - self._record["response_ids"] = _response_ids(raw_response) - return response - - -class _CapturingClient: - """Expose the OpenAI chat interface expected by the upstream helper.""" - - def __init__( - self, - client: Any, - record: dict[str, Any], - request_overrides: Mapping[str, Any], - ) -> None: - chat_type = type("_CapturingChat", (), {}) - self.chat = chat_type() - self.chat.completions = _CapturingCompletions( - client.chat.completions, record, request_overrides - ) - - -def _runtime_versions() -> dict[str, str]: - versions: dict[str, str] = {"python": sys.version} - for distribution in ("openai", "httpx", "jsonschema"): - try: - versions[distribution] = importlib.metadata.version(distribution) - except importlib.metadata.PackageNotFoundError: - versions[distribution] = "not-installed" - return versions - -def _capture_version_endpoint(base_url: str) -> dict[str, Any]: - version_url = f"{base_url.removesuffix('/v1').rstrip('/')}/version" - started_at = _utc_timestamp() - try: - httpx = importlib.import_module("httpx") - response = httpx.get(version_url, timeout=5.0) - safe_headers = { - key: value - for key, value in response.headers.items() - if key.lower() - in { - "content-type", - "date", - "server", - "x-request-id", - "x-sglang-version", - } - } - return { - "url": version_url, - "started_at": started_at, - "completed_at": _utc_timestamp(), - "status_code": response.status_code, - "headers": safe_headers, - "body": response.text, - } - except Exception as exc: - return { - "url": version_url, - "started_at": started_at, - "completed_at": _utc_timestamp(), - "error": {"type": type(exc).__name__, "message": str(exc)}, - } - - -def run_diagnostic_sequence( - *, - verifier_dir: Path, - base_url: str, - api_key: str, - model: str, - output_dir: Path, - temperature: float = DEFAULT_DIAGNOSTIC_TEMPERATURE, - seed: int = DEFAULT_DIAGNOSTIC_SEED, - sequence: Sequence[str] = DEFAULT_DIAGNOSTIC_SEQUENCE, -) -> None: - """Run ordered stock-helper requests and preserve client-visible evidence.""" - report_path = output_dir / DIAGNOSTIC_REPORT_FILENAME - records: list[dict[str, Any]] = [] - report: dict[str, Any] = { - "model": model, - "base_url": base_url, - "started_at": _utc_timestamp(), - "controls": { - "temperature": temperature, - "seed": seed, - "sequence": list(sequence), - }, - "runtime_versions": _runtime_versions(), - "version_endpoint": _capture_version_endpoint(base_url), - "results": records, - } - sys.path.insert(0, str(verifier_dir)) - client: Any = None - try: - validator = importlib.import_module("tests.tool_call_json_schema.validator") - report["parser"] = { - "module": validator.__name__, - "file": str(Path(validator.__file__).resolve()), - } - cases = validator.load_cases( - verifier_dir / "testdata" / "walle_validator_cases" / "validator_cases" - ) - selected = validator.select_cases( - cases, - selection="object", - requested_cases=set(), - max_cases=1, - ) - if len(selected) != 1: - raise ValueError( - f"diagnostic expected one selected case, found {len(selected)}" - ) - case, schema, selection_reason = selected[0] - report["case"] = { - "suite": case.suite, - "line": case.line, - "selection_reason": selection_reason, - "schema": schema, - } - client = validator.make_client(base_url, api_key, 120) - mode_occurrences = {"unary": 0, "stream": 0} - for index, mode in enumerate(sequence, start=1): - mode_occurrences[mode] += 1 - record: dict[str, Any] = { - "index": index, - "mode": mode, - "mode_occurrence": mode_occurrences[mode], - "temperature_state": ( - "cold" if mode_occurrences[mode] == 1 else "warm" - ), - "sampling_controls": { - "temperature": temperature, - "seed": seed, - }, - "started_at": _utc_timestamp(), - "response_ids": [], - } - records.append(record) - try: - response = validator.send_tool_schema( - _CapturingClient( - client, - record, - {"temperature": temperature, "seed": seed}, - ), - model, - schema, - 2048, - False, - "none", - stream=mode == "stream", - ) - valid, validation_message = validator.validate_arguments( - schema, response.arguments - ) - record["parser_output"] = { - "accepted": response.accepted, - "message": response.message, - "arguments": response.arguments, - "arguments_valid": valid, - "validation_message": validation_message, - "http_status": response.http_status, - "error_type": response.error_type, - } - except Exception as exc: - record["diagnostic_error"] = { - "type": type(exc).__name__, - "message": str(exc), - } - finally: - record["completed_at"] = _utc_timestamp() - except Exception as exc: - report["setup_error"] = { - "type": type(exc).__name__, - "message": str(exc), - } - finally: - if client is not None: - try: - client.close() - except Exception as exc: - report["client_close_error"] = { - "type": type(exc).__name__, - "message": str(exc), - } - report["completed_at"] = _utc_timestamp() - report_path.write_text( - json.dumps(report, indent=2) + "\n", - encoding="utf-8", - ) - sys.path.pop(0) def prepare_compatibility_path(output_dir: Path) -> Path: @@ -335,6 +38,10 @@ def build_pytest_command( "-m", "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "--reruns", + "3", + "--reruns-delay", + "2", "--base-url", base_url, "--api-key", @@ -464,10 +171,6 @@ def run_evaluation( model: str, output_dir: Path, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, - diagnostic: bool = False, - diagnostic_temperature: float = DEFAULT_DIAGNOSTIC_TEMPERATURE, - diagnostic_seed: int = DEFAULT_DIAGNOSTIC_SEED, - diagnostic_sequence: Sequence[str] = DEFAULT_DIAGNOSTIC_SEQUENCE, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -479,23 +182,6 @@ def run_evaluation( complete_pass = False try: native_report.unlink(missing_ok=True) - if diagnostic: - try: - run_diagnostic_sequence( - verifier_dir=verifier_dir, - base_url=base_url, - api_key=api_key, - model=model, - output_dir=output_dir, - temperature=diagnostic_temperature, - seed=diagnostic_seed, - sequence=diagnostic_sequence, - ) - except Exception as exc: - print( - f"WARNING: Kimi diagnostic collection failed: {exc}", - file=sys.stderr, - ) completed = subprocess.run( build_pytest_command( base_url=base_url, @@ -540,13 +226,6 @@ def _positive_int(value: str) -> int: raise argparse.ArgumentTypeError("must be a positive integer") return parsed -def _diagnostic_sequence(value: str) -> tuple[str, ...]: - sequence = tuple(item.strip() for item in value.split(",") if item.strip()) - if not sequence or any(mode not in {"unary", "stream"} for mode in sequence): - raise argparse.ArgumentTypeError( - "must be a comma-separated sequence of unary and stream" - ) - return sequence def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: @@ -562,26 +241,6 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS ) parser.add_argument("--integration-error") - parser.add_argument( - "--diagnostic", - action="store_true", - help="Run ordered raw request diagnostics before the unchanged verifier", - ) - parser.add_argument( - "--diagnostic-temperature", - type=float, - default=DEFAULT_DIAGNOSTIC_TEMPERATURE, - ) - parser.add_argument( - "--diagnostic-seed", - type=int, - default=DEFAULT_DIAGNOSTIC_SEED, - ) - parser.add_argument( - "--diagnostic-sequence", - type=_diagnostic_sequence, - default=DEFAULT_DIAGNOSTIC_SEQUENCE, - ) args = parser.parse_args(argv) if args.integration_error is None: missing = [ @@ -621,10 +280,6 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, output_dir=args.output_dir, timeout_seconds=args.timeout_seconds, - diagnostic=args.diagnostic, - diagnostic_temperature=args.diagnostic_temperature, - diagnostic_seed=args.diagnostic_seed, - diagnostic_sequence=args.diagnostic_sequence, ) return 0 if passed else 1 diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index 85aef1e9c6..a71d3d962f 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -62,6 +62,10 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: "-m", "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "--reruns", + "3", + "--reruns-delay", + "2", "--base-url", "http://127.0.0.1:8000/v1", "--api-key", @@ -236,197 +240,3 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" assert _n_eff(output_dir) == 0 - - -def test_diagnostic_defaults_are_explicit_and_stock_command_is_unchanged( - tmp_path: Path, -) -> None: - args = kve.parse_args( - [ - "--verifier-dir", - str(tmp_path), - "--base-url", - "http://localhost/v1", - "--model", - "model-a", - "--output-dir", - str(tmp_path / "output"), - "--diagnostic", - ] - ) - assert args.diagnostic_temperature == 0 - assert args.diagnostic_seed == 1 - assert args.diagnostic_sequence == ( - "unary", - "unary", - "stream", - "stream", - "stream", - "unary", - ) - assert "--diagnostic" not in kve.build_pytest_command( - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - report_path=tmp_path / "report.json", - ) - - -def test_diagnostic_captures_payload_sequence_and_raw_modes( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - verifier_dir = tmp_path / "verifier" - (verifier_dir / "testdata/walle_validator_cases/validator_cases").mkdir( - parents=True - ) - output_dir = tmp_path / "output" - output_dir.mkdir() - requests: list[dict[str, Any]] = [] - - class Raw: - def __init__(self, value: dict[str, Any]) -> None: - self.value = value - - def model_dump(self, *, mode: str) -> dict[str, Any]: - assert mode == "json" - return self.value - - class Completions: - def create(self, **request: Any) -> Any: - requests.append(request) - raw = { - "id": f"response-{len(requests)}", - "choices": [ - { - "message": { - "reasoning_content": "reasoning", - "tool_calls": [], - } - } - ], - } - if request.get("stream"): - return iter([Raw(raw)]) - return Raw(raw) - - client = SimpleNamespace( - chat=SimpleNamespace(completions=Completions()), - close=lambda: None, - ) - - def send_tool_schema( - capturing_client: Any, - model: str, - schema: Any, - max_tokens: int, - thinking: bool, - think_mode: str, - *, - stream: bool, - ) -> SimpleNamespace: - response = capturing_client.chat.completions.create( - model=model, - messages=[{"role": "user", "content": "identical"}], - tools=[{"type": "function", "function": {"parameters": schema}}], - max_tokens=max_tokens, - stream=stream, - ) - if stream: - list(response) - return SimpleNamespace( - accepted=True, - message="parsed", - arguments="{}", - http_status=None, - error_type=None, - ) - - fake_validator = SimpleNamespace( - __name__="tests.tool_call_json_schema.validator", - __file__=str(verifier_dir / "validator.py"), - load_cases=lambda path: ["case"], - select_cases=lambda cases, **kwargs: [ - ( - SimpleNamespace(suite="suite", line=1), - {"type": "object"}, - "object_parameter_schema", - ) - ], - make_client=lambda *args: client, - send_tool_schema=send_tool_schema, - validate_arguments=lambda schema, arguments: (True, "valid"), - ) - monkeypatch.setattr(kve.importlib, "import_module", lambda name: fake_validator) - monkeypatch.setattr( - kve, - "_capture_version_endpoint", - lambda base_url: {"status_code": 200, "body": "v1"}, - ) - - kve.run_diagnostic_sequence( - verifier_dir=verifier_dir, - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, - temperature=0.25, - seed=7, - sequence=("unary", "stream", "unary"), - ) - - report = json.loads((output_dir / kve.DIAGNOSTIC_REPORT_FILENAME).read_text()) - assert report["controls"] == { - "temperature": 0.25, - "seed": 7, - "sequence": ["unary", "stream", "unary"], - } - assert [row["mode"] for row in report["results"]] == [ - "unary", - "stream", - "unary", - ] - assert [row["temperature_state"] for row in report["results"]] == [ - "cold", - "cold", - "warm", - ] - assert all( - request["temperature"] == 0.25 and request["seed"] == 7 - for request in requests - ) - assert report["results"][0]["request_payload"] == requests[0] - assert report["results"][0]["raw_response"]["choices"][0]["message"][ - "reasoning_content" - ] == "reasoning" - assert report["results"][1]["raw_chunks"][0]["id"] == "response-2" - assert report["results"][1]["response_ids"] == ["response-2"] - assert report["results"][2]["parser_output"]["arguments_valid"] is True - - -def test_diagnostic_failure_does_not_change_stock_score( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - output_dir = tmp_path / "output" - - def broken_diagnostic(**kwargs: Any) -> None: - raise RuntimeError("diagnostic failed") - - def fake_run( - command: list[str], *, cwd: Path, check: bool, timeout: int - ) -> SimpleNamespace: - Path(command[command.index("--tool-json-report") + 1]).write_text( - json.dumps(_report()) - ) - return SimpleNamespace(returncode=0) - - monkeypatch.setattr(kve, "run_diagnostic_sequence", broken_diagnostic) - monkeypatch.setattr(kve.subprocess, "run", fake_run) - assert kve.run_evaluation( - verifier_dir=tmp_path, - base_url="http://localhost/v1", - api_key="EMPTY", - model="model-a", - output_dir=output_dir, - diagnostic=True, - ) - assert _score(output_dir) == 1.0 \ No newline at end of file diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1dfafca2db..cbcf25b7e3 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -202,7 +202,12 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: export EVAL_SUITE=kimi_tool_call_schema echo "DISPATCH=kimi-vendor SUITE=$EVAL_SUITE" } -run_lm_eval() { echo "DISPATCH=lm-eval SUITE=${EVAL_SUITE:-unset}"; } +run_lm_eval() { + echo "DISPATCH=lm-eval SUITE=${EVAL_SUITE:-unset} COMPLETED=${EVAL_COMPLETED_SUITE:-unset}" +} +append_lm_eval_summary() { + echo "METADATA=${EVAL_COMPLETED_SUITE:-gsm8k}" +} export EVAL_MAX_MODEL_LEN=16384 export EVAL_CONCURRENT_REQUESTS="" export EVAL_ONLY=false @@ -210,8 +215,12 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: unset EVAL_SUITE export EVAL_FRAMEWORK=kimi-vendor run_eval --port 8888 +printf 'KIMI_COMPLETED=%s\n' "${EVAL_COMPLETED_SUITE:-unset}" +append_lm_eval_summary export EVAL_FRAMEWORK=lm-eval run_eval --port 8888 +printf 'LM_COMPLETED=%s\n' "${EVAL_COMPLETED_SUITE:-unset}" +append_lm_eval_summary printf 'FINAL_SUITE=%s\n' "${EVAL_SUITE-unset}" ''' result = subprocess.run( @@ -224,7 +233,11 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: assert result.returncode == 0, result.stderr assert "DISPATCH=kimi-vendor SUITE=kimi_tool_call_schema" in result.stdout - assert "DISPATCH=lm-eval SUITE=unset" in result.stdout + assert "KIMI_COMPLETED=kimi_tool_call_schema" in result.stdout + assert "METADATA=kimi_tool_call_schema" in result.stdout + assert "DISPATCH=lm-eval SUITE=unset COMPLETED=unset" in result.stdout + assert "LM_COMPLETED=unset" in result.stdout + assert "METADATA=gsm8k" in result.stdout assert "FINAL_SUITE=unset" in result.stdout @@ -232,7 +245,7 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: script = r''' source "$BENCHMARK_LIB" run_kimi_vendor_eval() { echo "DISPATCH=$EVAL_SUITE"; } -append_lm_eval_summary() { echo "METADATA=$EVAL_SUITE"; } +append_lm_eval_summary() { echo "METADATA=$EVAL_COMPLETED_SUITE"; } export EVAL_FRAMEWORK=kimi-vendor export EVAL_ONLY=true export IS_AGENTIC=1 @@ -535,6 +548,7 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: assert "PYTHON_ARG=<--target>" in result.stdout assert f"PYTHON_ARG=<{runtime_dir}>" in result.stdout + assert "PYTHON_ARG=" in result.stdout assert "--break-system-packages" not in result.stdout @@ -740,7 +754,7 @@ def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: "CONC": "7", "KV_OFFLOADING": "none", } - for key in ("EVAL_SUITE", "EVAL_TASKS_DIR"): + for key in ("EVAL_COMPLETED_SUITE", "EVAL_SUITE", "EVAL_TASKS_DIR"): env.pop(key, None) env.update(overrides) subprocess.run(["bash", "-c", script], env=env, check=True) @@ -800,6 +814,17 @@ def test_summary_metadata_prefers_explicit_suite_then_task_basename( assert explicit["eval_suite"] == "kimi_tool_call_schema" +def test_summary_metadata_prefers_completed_eval_identity(tmp_path: Path) -> None: + meta = _summary_metadata( + tmp_path, + EVAL_COMPLETED_SUITE="kimi_tool_call_schema", + EVAL_SUITE="stale_input_selector", + EVAL_TASKS_DIR="/tmp/ignored.yaml", + ) + + assert meta["eval_suite"] == "kimi_tool_call_schema" + + def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: script = r''' set -u diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 3cac4d6b48..13f1cefe2e 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -42,23 +42,27 @@ def test_build_row_preserves_explicit_eval_suite() -> None: assert row["eval_suite"] == "kimi_tool_call_schema" -def _write_lm_eval_result(path: Path, score: float) -> None: +def _write_lm_eval_result( + path: Path, + score: float, + task: str = "gsm8k", +) -> None: path.write_text(json.dumps({ "lm_eval_version": "0.4.0", "model_name": "test-model", "results": { - "gsm8k": { + task: { "exact_match,strict-match": score, "exact_match_stderr,strict-match": 0.01, }, }, "configs": { - "gsm8k": { + task: { "metric_list": [{"metric": "exact_match"}], "filter_list": [{"name": "strict-match"}], }, }, - "n-samples": {"gsm8k": {"effective": 10}}, + "n-samples": {task: {"effective": 10}}, })) @@ -208,19 +212,23 @@ def test_collect_eval_rows_does_not_resurrect_stale_valid_result( artifact_dir = tmp_path / "eval_retry" artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text(json.dumps({ - "eval_suite": "gsm8k", + "eval_suite": "kimi_tool_call_schema", })) - stale_path = artifact_dir / "results_older.json" - _write_lm_eval_result(stale_path, 1.0) - current_path = artifact_dir / "results_current.json" - _write_lm_eval_result(current_path, 0.0) + stale_path = ( + artifact_dir / "results_kimi_vendor_2026-08-12T01-00-00.000000.json" + ) + _write_lm_eval_result(stale_path, 1.0, task="kimi_tool_call_schema") + current_path = ( + artifact_dir / "results_kimi_vendor_2026-08-12T02-00-00.000000.json" + ) + _write_lm_eval_result(current_path, 0.0, task="kimi_tool_call_schema") result = json.loads(current_path.read_text()) result["integration_error"] = { "type": "RuntimeError", "message": "vendor verifier checkout failed", } current_path.write_text(json.dumps(result)) - stale_path.touch() current_path.touch() + stale_path.touch() assert collect_eval_rows(tmp_path) == [] \ No newline at end of file From 8952cdc01284d8039b7b37a009a6f354d1eef8f3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:32:06 -0500 Subject: [PATCH 25/99] fix: preserve tool eval dispatch contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留工具调用评估的工作流路由、框架选择和专用评估配方契约。 --- .github/workflows/e2e-tests.yml | 3 ++- .../agg-b200-tp8dp2-latency-dspark-eval-agentic.yaml | 1 - ...200-tp8dp2-vllm-simple-offload-dspark-eval-agentic.yaml | 1 - benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh | 2 +- benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh | 2 +- benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh | 2 +- .../single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh | 2 +- benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh | 2 +- benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh | 2 +- utils/evals/test_run_eval_dispatch.py | 7 +++++++ 10 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index dc4bbb665a..f361b23144 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -471,7 +471,8 @@ jobs: dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} kv-offloading: ${{ matrix.config.kv-offloading }} - kv-offload-backend: ${{ matrix.config.kv-offload-backend }} + kv-offload-backend: ${{ matrix.config['kv-offload-backend'].name }} + kv-offload-backend-metadata: ${{ matrix.config['kv-offload-backend'] && toJson(matrix.config['kv-offload-backend']) || '' }} total-cpu-dram-gb: ${{ matrix.config.total-cpu-dram-gb }} duration: ${{ inputs.agentx-fast && '1200' || (inputs.duration-override != '' && inputs.duration-override || matrix.config.duration) }} agentx-fast: ${{ inputs.agentx-fast }} diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-latency-dspark-eval-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-latency-dspark-eval-agentic.yaml index f562682522..ffec91ad73 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-latency-dspark-eval-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-latency-dspark-eval-agentic.yaml @@ -93,7 +93,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-vllm-simple-offload-dspark-eval-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-vllm-simple-offload-dspark-eval-agentic.yaml index a3ae8342b9..10287471d9 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-vllm-simple-offload-dspark-eval-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-b200-tp8dp2-vllm-simple-offload-dspark-eval-agentic.yaml @@ -94,7 +94,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh index b735220fc7..c1e3c924d7 100644 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh @@ -11,7 +11,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION diff --git a/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh index 88d9078819..08a4da1aa8 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh @@ -6,7 +6,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION PORT EVAL_ONLY diff --git a/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh index 1851139dab..1002073712 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh @@ -4,7 +4,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING RESULT_DIR DURATION EP_SIZE DP_ATTENTION PORT EVAL_ONLY diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh index 804d65b08e..7f229cfae4 100644 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh @@ -8,7 +8,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh index aed9047466..4bda81c58a 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh @@ -6,7 +6,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh index 123e1c4acf..d9601bdd4d 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh @@ -8,7 +8,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE \ diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index cbcf25b7e3..14a5005374 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1552,6 +1552,13 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + assert forwarded["kv-offload-backend"] == ( + "${{ matrix.config['kv-offload-backend'].name }}" + ) + assert forwarded["kv-offload-backend-metadata"] == ( + "${{ matrix.config['kv-offload-backend'] && " + "toJson(matrix.config['kv-offload-backend']) || '' }}" + ) def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: From ba5b36350c21cb0922dff8447c13e1da1bd32f13 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:37:14 -0500 Subject: [PATCH 26/99] fix: enable structured Qwen tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为 H100 和 H200 的 Qwen3.5 SGLang 启动命令启用推理解析器和工具调用解析器。 --- .../single_node/agentic/qwen3.5_fp8_h100_mtp.sh | 2 ++ .../single_node/agentic/qwen3.5_fp8_h200_mtp.sh | 2 ++ utils/evals/test_run_eval_dispatch.py | 11 +++++++++++ 3 files changed, 15 insertions(+) diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh index 95dae3834f..bdd8feb2f5 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh @@ -161,6 +161,8 @@ SGLANG_CMD=( --scheduler-recv-interval "$SCHEDULER_RECV_INTERVAL" --tokenizer-worker-num 6 --tokenizer-path "$MODEL" + --reasoning-parser qwen3 + --tool-call-parser qwen3_coder --enable-metrics "${SPEC_ARGS[@]}" "${CACHE_ARGS[@]}" diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh index 62982a3974..6ba7a1fb15 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh @@ -163,6 +163,8 @@ SGLANG_CMD=( --scheduler-recv-interval "$SCHEDULER_RECV_INTERVAL" --tokenizer-worker-num 6 --tokenizer-path "$MODEL" + --reasoning-parser qwen3 + --tool-call-parser qwen3_coder --enable-metrics "${SPEC_ARGS[@]}" "${CACHE_ARGS[@]}" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 14a5005374..b0cdc78133 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -19,6 +19,10 @@ BENCHMARK_LIB = REPO_ROOT / "benchmarks" / "benchmark_lib.sh" MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" +QWEN_SGLANG_MTP_LAUNCHERS = ( + REPO_ROOT / "benchmarks" / "single_node" / "agentic" / "qwen3.5_fp8_h100_mtp.sh", + REPO_ROOT / "benchmarks" / "single_node" / "agentic" / "qwen3.5_fp8_h200_mtp.sh", +) _SCRIPT = r''' source "$BENCHMARK_LIB" @@ -1545,6 +1549,13 @@ def test_eval_limit_full_and_zero_accepted(tmp_path): assert "--slice" not in argv +def test_qwen_sglang_launchers_expose_structured_tool_calls() -> None: + for launcher in QWEN_SGLANG_MTP_LAUNCHERS: + command = launcher.read_text() + assert "--reasoning-parser qwen3" in command + assert "--tool-call-parser qwen3_coder" in command + + def test_agentic_eval_workflow_forwards_runner_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) forwarded = workflow["jobs"]["test-sweep-agentic-evals"]["with"] From a4f69b9b7ced80205244ba1958c10484cb4b3028 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:45:23 -0500 Subject: [PATCH 27/99] fix: preserve single-node eval topology metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留单节点评估结果中的专家并行拓扑元数据,避免将 EP8 错报为 EP1。 --- benchmarks/benchmark_lib.sh | 67 +++++++++++++++++++++------ utils/evals/test_run_eval_dispatch.py | 9 ++++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index fe81bccc4b..983f79465b 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -892,20 +892,57 @@ _install_lm_eval_deps() { fi } -_require_kimi_vendor_python() { +_prepare_kimi_vendor_python() { + KIMI_VENDOR_PYTHON=python3 + KIMI_VENDOR_PYTHON_CLEANUP_DIR="" + export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR + if python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))'; then return 0 fi - local python_version - python_version="$(python3 -c 'import platform; print(platform.python_version())' 2>/dev/null || printf 'unavailable')" - echo "ERROR: Kimi Vendor Verifier requires Python >=3.12 (python3 is ${python_version})" >&2 - return 2 + local python_dir uv_prefix uv_bin venv_dir prepare_rc=0 + python_dir="$(mktemp -d /tmp/kimi-vendor-python-XXXXXX)" || { + echo "ERROR: could not create a temporary Python directory for Kimi-Vendor-Verifier" >&2 + return 1 + } + KIMI_VENDOR_PYTHON_CLEANUP_DIR="$python_dir" + export KIMI_VENDOR_PYTHON_CLEANUP_DIR + + uv_prefix="${python_dir}/uv" + uv_bin="${uv_prefix}/bin/uv" + venv_dir="${python_dir}/venv" + python3 -m pip install -q --no-cache-dir --prefix "$uv_prefix" "uv==0.11.33" \ + || prepare_rc=$? + if [ "$prepare_rc" -eq 0 ] && [ ! -x "$uv_bin" ]; then + echo "ERROR: pinned uv installation did not create ${uv_bin}" >&2 + prepare_rc=1 + fi + if [ "$prepare_rc" -eq 0 ]; then + UV_CACHE_DIR="${python_dir}/uv-cache" \ + UV_PYTHON_INSTALL_DIR="${python_dir}/python" \ + "$uv_bin" venv --python 3.12 --seed "$venv_dir" \ + || prepare_rc=$? + fi + if [ "$prepare_rc" -eq 0 ] && [ ! -x "${venv_dir}/bin/python" ]; then + echo "ERROR: pinned uv did not create the Kimi verifier Python interpreter" >&2 + prepare_rc=1 + fi + if [ "$prepare_rc" -ne 0 ]; then + rm -rf "$python_dir" || true + KIMI_VENDOR_PYTHON=python3 + KIMI_VENDOR_PYTHON_CLEANUP_DIR="" + export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR + return "$prepare_rc" + fi + + KIMI_VENDOR_PYTHON="${venv_dir}/bin/python" + export KIMI_VENDOR_PYTHON } _install_kimi_vendor_eval_deps() { local target_dir="$1" - python3 -m pip install -q --no-cache-dir --target "$target_dir" \ + "${KIMI_VENDOR_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ "httpx[http2]==0.28.1" \ "openai==2.14.0" \ "jsonschema==4.25.1" \ @@ -934,7 +971,7 @@ _prepare_kimi_vendor_verifier() { return 1 } - python3 - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? + "${KIMI_VENDOR_PYTHON:-python3}" - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? from pathlib import Path import re import socket @@ -1220,9 +1257,9 @@ _run_kimi_tool_call_schema_eval() { export EVAL_RESULT_DIR="$results_dir" local setup_rc=0 integration_error="" - _require_kimi_vendor_python || { + _prepare_kimi_vendor_python || { setup_rc=$? - integration_error="Kimi Vendor Verifier Python version check failed with exit code ${setup_rc}" + integration_error="Kimi Vendor Verifier Python runtime preparation failed with exit code ${setup_rc}" } if [ "$setup_rc" -eq 0 ]; then runtime_dir=$(_prepare_kimi_vendor_runtime) || { @@ -1239,7 +1276,8 @@ _run_kimi_tool_call_schema_eval() { } fi if [ "$setup_rc" -ne 0 ]; then - _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" + _cleanup_kimi_vendor_eval \ + "$runtime_dir" "$checkout_dir" "${KIMI_VENDOR_PYTHON_CLEANUP_DIR:-}" echo "ERROR: ${integration_error}" >&2 local artifact_rc=0 _write_kimi_vendor_integration_error \ @@ -1253,14 +1291,15 @@ _run_kimi_tool_call_schema_eval() { local eval_rc=0 PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 "$adapter_path" \ + "${KIMI_VENDOR_PYTHON:-python3}" "$adapter_path" \ --verifier-dir "$checkout_dir" \ --base-url "http://127.0.0.1:${port}/v1" \ --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ || eval_rc=$? - _cleanup_kimi_vendor_eval "$runtime_dir" "$checkout_dir" + _cleanup_kimi_vendor_eval \ + "$runtime_dir" "$checkout_dir" "${KIMI_VENDOR_PYTHON_CLEANUP_DIR:-}" return "$eval_rc" } @@ -1545,11 +1584,11 @@ _normalize_bool_json() { bridge_disagg_eval_metadata() { export TP="${PREFILL_TP:-${PREFILL_TP_SIZE:-${TP:-1}}}" export PREFILL_TP="${PREFILL_TP:-${PREFILL_TP_SIZE:-${TP:-1}}}" - export PREFILL_EP="$(_resolve_disagg_ep "${PREFILL_EP:-1}" "${PREFILL_ENABLE_EP:-false}" "${PREFILL_TP_SIZE:-${PREFILL_TP:-1}}")" + export PREFILL_EP="$(_resolve_disagg_ep "${PREFILL_EP:-${EP_SIZE:-${EP:-1}}}" "${PREFILL_ENABLE_EP:-false}" "${PREFILL_TP_SIZE:-${PREFILL_TP:-1}}")" export EP_SIZE="${PREFILL_EP}" export PREFILL_NUM_WORKERS="${PREFILL_NUM_WORKERS:-${xP:-1}}" export DECODE_TP="${DECODE_TP:-${DECODE_TP_SIZE:-${TP:-1}}}" - export DECODE_EP="$(_resolve_disagg_ep "${DECODE_EP:-1}" "${DECODE_ENABLE_EP:-false}" "${DECODE_TP_SIZE:-${DECODE_TP:-1}}")" + export DECODE_EP="$(_resolve_disagg_ep "${DECODE_EP:-${EP_SIZE:-${EP:-1}}}" "${DECODE_ENABLE_EP:-false}" "${DECODE_TP_SIZE:-${DECODE_TP:-1}}")" export DECODE_NUM_WORKERS="${DECODE_NUM_WORKERS:-${yD:-1}}" local prefill_dp="${PREFILL_DP_ATTN:-${PREFILL_DP_ATTENTION:-${PREFILL_ENABLE_DP:-false}}}" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index b0cdc78133..6652aa10b0 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -772,6 +772,15 @@ def test_summary_metadata_preserves_lm_eval_gsm8k_defaults(tmp_path: Path) -> No assert meta["conc"] == 7 +def test_summary_metadata_preserves_single_node_expert_parallelism( + tmp_path: Path, +) -> None: + meta = _summary_metadata(tmp_path, TP="8", EP_SIZE="8") + + assert meta["ep"] == 8 + assert meta["prefill_ep"] == 8 + assert meta["decode_ep"] == 8 + def test_run_lm_eval_exports_cli_task_path(tmp_path: Path) -> None: script = r''' source "$BENCHMARK_LIB" From 333a0322f75c10cfc335c74f4a8499086acf17bb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:51:12 -0500 Subject: [PATCH 28/99] test: cover isolated verifier runtime bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:覆盖旧版系统 Python 的隔离运行时引导、解释器路由和临时目录清理,并记录运行时契约。 --- utils/evals/EVALS.md | 14 ++- utils/evals/test_run_eval_dispatch.py | 156 ++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 9c1375e7c0..d1825d433d 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -81,17 +81,22 @@ configuration, tool-call schema tests, and bundled Walle cases. InferenceX does not install the verifier package or reimplement its request, streaming, or validation logic. -Python 3.12 or newer is required. The runner installs the minimal pinned runtime +System Python 3.12 or newer is preferred and used directly. On older images, +the runner uses the existing system `pip` to install pinned `uv==0.11.33` under +a temporary prefix, then provisions an isolated Python 3.12 virtual environment. +The selected interpreter installs the minimal pinned verifier runtime (`httpx[http2]`, `openai`, `jsonschema`, `pytest`, and -`pytest-rerunfailures`) into a temporary isolated package directory, then runs -upstream -`tests/tool_call_json_schema/test_tool_call_json_schema.py` with: +`pytest-rerunfailures`) into a separate temporary package directory, then runs +upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; - `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; - the upstream-recommended `--reruns 3 --reruns-delay 2`; - the bundled Walle case directory and `--tool-json-report`. +The temporary Python runtime, package directory, and verifier checkout are +removed after both successful and failed runs. + The selection is `TestAdditionalProperties:1`, parametrized upstream in non-streaming and streaming modes. Pytest makes one initial attempt and up to three reruns of each failing mode, with a two-second delay before each rerun. @@ -144,6 +149,7 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | +| `_prepare_kimi_vendor_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime on older images | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | | `_prepare_kimi_vendor_verifier` | Downloads and safely extracts a fresh subset of the pinned source archive | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 6652aa10b0..b3ab4ee8f1 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -293,9 +293,15 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, ) -> None: results_dir = tmp_path / "results" + python_dir = tmp_path / "python" script = r''' source "$BENCHMARK_LIB" -_require_kimi_vendor_python() { :; } +_prepare_kimi_vendor_python() { + mkdir "$PYTHON_DIR" + KIMI_VENDOR_PYTHON=/unusable/bootstrap/python + KIMI_VENDOR_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR +} _prepare_kimi_vendor_runtime() { return 12; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'SETUP_RC=%s\n' "$?" @@ -304,6 +310,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "RESULTS_DIR": str(results_dir), + "PYTHON_DIR": str(python_dir), "MODEL": "test-model", "IS_MULTINODE": "false", "KV_OFFLOADING": "none", @@ -338,6 +345,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( ) assert score_result["integration_error"]["message"] == message assert not (results_dir / "kimi_vendor_report.json").exists() + assert not python_dir.exists() _KIMI_VERIFIER_REQUIRED_FILES = { @@ -531,11 +539,121 @@ def test_kimi_vendor_verifier_rejects_unsafe_archive_members(tmp_path: Path) -> assert not (tmp_path / "escaped").exists() +def test_kimi_vendor_uses_system_python_fast_path() -> None: + script = r''' +source "$BENCHMARK_LIB" +python3() { + printf 'SYSTEM_PYTHON_ARG=<%s>\n' "$@" + [ "$1" = "-c" ] +} +mktemp() { echo "UNEXPECTED_MKTEMP"; return 99; } +KIMI_VENDOR_PYTHON=/previous/python +KIMI_VENDOR_PYTHON_CLEANUP_DIR=/previous/runtime +_prepare_kimi_vendor_python +printf 'SELECTED_PYTHON=<%s>\n' "$KIMI_VENDOR_PYTHON" +printf 'PYTHON_CLEANUP=<%s>\n' "$KIMI_VENDOR_PYTHON_CLEANUP_DIR" +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=True, + ) + + assert "SYSTEM_PYTHON_ARG=<-c>" in result.stdout + assert "SELECTED_PYTHON=" in result.stdout + assert "PYTHON_CLEANUP=<>" in result.stdout + assert "UNEXPECTED_MKTEMP" not in result.stdout + + +def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( + tmp_path: Path, +) -> None: + log_path = tmp_path / "bootstrap.log" + fake_uv = tmp_path / "fake-uv" + fake_uv.write_text( + r'''#!/usr/bin/env bash +printf 'UV_CACHE_DIR=<%s>\n' "$UV_CACHE_DIR" >> "$KIMI_LOG" +printf 'UV_PYTHON_INSTALL_DIR=<%s>\n' "$UV_PYTHON_INSTALL_DIR" >> "$KIMI_LOG" +printf 'UV_ARG=<%s>\n' "$@" >> "$KIMI_LOG" +venv_dir="${!#}" +mkdir -p "$venv_dir/bin" +cat > "$venv_dir/bin/python" <<'PYTHON' +#!/usr/bin/env bash +printf 'SELECTED_PYTHON_ARG=<%s>\n' "$@" >> "$KIMI_LOG" +PYTHON +chmod +x "$venv_dir/bin/python" +''' + ) + fake_uv.chmod(0o755) + script = r''' +source "$BENCHMARK_LIB" +python3() { + if [ "$1" = "-c" ]; then + printf 'VERSION_CHECK\n' >> "$KIMI_LOG" + return 1 + fi + printf 'SYSTEM_PYTHON_ARG=<%s>\n' "$@" >> "$KIMI_LOG" + local prefix="" + while [[ $# -gt 0 ]]; do + if [ "$1" = "--prefix" ]; then + prefix="$2" + break + fi + shift + done + mkdir -p "$prefix/bin" + cp "$FAKE_UV" "$prefix/bin/uv" + chmod +x "$prefix/bin/uv" +} +_prepare_kimi_vendor_python +cleanup_dir="$KIMI_VENDOR_PYTHON_CLEANUP_DIR" +printf 'SELECTED_PYTHON=<%s>\n' "$KIMI_VENDOR_PYTHON" +printf 'PYTHON_CLEANUP=<%s>\n' "$cleanup_dir" +runtime_dir="$TEST_ROOT/runtime" +mkdir "$runtime_dir" +_install_kimi_vendor_eval_deps "$runtime_dir" +_cleanup_kimi_vendor_eval "$runtime_dir" "$cleanup_dir" +[ ! -e "$runtime_dir" ] && [ ! -e "$cleanup_dir" ] && printf 'CLEANED\n' +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "FAKE_UV": str(fake_uv), + "KIMI_LOG": str(log_path), + "TEST_ROOT": str(tmp_path), + }, + text=True, + capture_output=True, + check=True, + ) + log = log_path.read_text() + + assert "VERSION_CHECK" in log + assert "SYSTEM_PYTHON_ARG=<--prefix>" in log + assert "SYSTEM_PYTHON_ARG=" in log + assert "UV_ARG=" in log + assert "UV_ARG=<--python>" in log + assert "UV_ARG=<3.12>" in log + assert "UV_ARG=<--seed>" in log + assert "UV_CACHE_DIR=" in log + assert "SELECTED_PYTHON_ARG=" in log + assert "SELECTED_PYTHON= None: runtime_dir = tmp_path / "runtime" script = r''' source "$BENCHMARK_LIB" -python3() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +KIMI_VENDOR_PYTHON=selected_python _install_kimi_vendor_eval_deps "$RUNTIME_DIR" ''' result = subprocess.run( @@ -559,7 +677,7 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: def test_kimi_vendor_surfaces_failure_artifact_error(tmp_path: Path) -> None: script = r''' source "$BENCHMARK_LIB" -_require_kimi_vendor_python() { return 12; } +_prepare_kimi_vendor_python() { return 12; } _write_kimi_vendor_integration_error() { return 23; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'EVAL_RC=%s\n' "$?" @@ -590,20 +708,33 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( results_dir = tmp_path / "results" verifier_dir = tmp_path / "verifier" runtime_dir = tmp_path / "runtime" - runtime_dir.mkdir() + python_dir = tmp_path / "python" verifier_dir.mkdir() script = r''' source "$BENCHMARK_LIB" -_require_kimi_vendor_python() { :; } -_prepare_kimi_vendor_runtime() { printf '%s\n' "$RUNTIME_DIR"; } +_prepare_kimi_vendor_python() { + mkdir "$PYTHON_DIR" + KIMI_VENDOR_PYTHON=selected_python + KIMI_VENDOR_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR +} +_prepare_kimi_vendor_runtime() { + mkdir "$RUNTIME_DIR" + _install_kimi_vendor_eval_deps "$RUNTIME_DIR" >&2 + printf '%s\n' "$RUNTIME_DIR" +} _prepare_kimi_vendor_verifier() { printf 'CHECKOUT=%s@%s\n' "$1" "$2" >&2 + "$KIMI_VENDOR_PYTHON" - "$1" "$2" "$VERIFIER_DIR" <<'PY' >&2 +archive extraction +PY printf '%s\n' "$VERIFIER_DIR" } -python3() { - printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" - printf 'PYTHON_ARG=<%s>\n' "$@" +selected_python() { + printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" >&2 + printf 'PYTHON_ARG=<%s>\n' "$@" >&2 } +python3() { echo "SYSTEM_PYTHON_UNEXPECTED" >&2; return 99; } run_kimi_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" @@ -615,6 +746,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( "VERIFIER_DIR": str(verifier_dir), "MODEL": "test-model", "RUNTIME_DIR": str(runtime_dir), + "PYTHON_DIR": str(python_dir), "OPENAI_API_KEY": "must-not-be-forwarded", "KV_OFFLOADING": "none", "IS_MULTINODE": "true", @@ -641,6 +773,10 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( "CHECKOUT=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" "@b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" ) in output + assert "PYTHON_ARG=<->" in output + assert ( + "PYTHON_ARG=" in output + ) for value in ( adapter, verifier_dir, @@ -651,10 +787,12 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( ): assert f"PYTHON_ARG=<{value}>" in output assert "must-not-be-forwarded" not in output + assert "SYSTEM_PYTHON_UNEXPECTED" not in output assert "EVAL_SUITE=kimi_tool_call_schema" in output assert f"EVAL_RESULT_DIR={results_dir}" in output assert not (tmp_path / "runtime").exists() assert not verifier_dir.exists() + assert not python_dir.exists() def test_run_lm_eval_rejects_missing_option_value(): From 26c0154135a4cb03a5c2cc54113a287695d87f7a Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:53:19 -0500 Subject: [PATCH 29/99] fix: configure DSV4 tool call parsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为 H200 聚合式 DeepSeek-V4-Pro AgentX 配置启用原生推理解析器和工具调用解析器。 --- .../sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml index 816b59c366..1b6101504a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml @@ -72,6 +72,8 @@ backend: served-model-name: "deepseek-ai/DeepSeek-V4-Pro" enable-metrics: true trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 stream-interval: 50 watchdog-timeout: 1000000 mem-fraction-static: 0.88 From a2ad629ac8eabf2a2137fc367402be4f4ca12252 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:07:32 -0500 Subject: [PATCH 30/99] fix: configure sglang tool call parsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:配置 SGLang 工具调用解析器。 --- .../deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml | 2 ++ .../disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml | 4 ++++ .../agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml | 4 ++++ .../disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml | 4 ++++ .../disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml | 4 ++++ .../disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml | 4 ++++ .../qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml | 2 ++ .../qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml | 2 ++ .../sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml | 2 ++ .../agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml | 2 ++ .../agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml | 2 ++ .../agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml | 2 ++ .../agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml | 2 ++ .../agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml | 2 ++ .../agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml | 2 ++ .../agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml | 2 ++ ...gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml | 4 ++++ ...b300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml | 4 ++++ ...gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml | 4 ++++ ...gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml | 4 ++++ ...gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml | 4 ++++ ...-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml | 4 ++++ ...gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml | 4 ++++ 23 files changed, 70 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml index 7bb82d2b5b..904bed7305 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml @@ -91,6 +91,8 @@ backend: served-model-name: "deepseek-ai/DeepSeek-V4-Pro" enable-metrics: true trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 stream-interval: 50 watchdog-timeout: 1000000 mem-fraction-static: 0.90 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml index 709962936f..56e1375365 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml @@ -134,6 +134,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -169,6 +171,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml index e414387655..35fb27f95f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml @@ -134,6 +134,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -169,6 +171,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 4 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml index 74b371e94e..00842b8bef 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml @@ -131,6 +131,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -166,6 +168,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml index 6b585fba80..8903781a74 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml @@ -131,6 +131,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -166,6 +168,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml index b2cac21c78..7ceb760b0c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml @@ -131,6 +131,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -166,6 +168,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: deepseek-v4 + tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml index 716ac5d4cc..4c10d2c40a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml @@ -29,6 +29,8 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 2 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml index 917f7e5765..44b4819990 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml @@ -29,6 +29,8 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml index c85a63efb9..5cef6af56f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml @@ -41,6 +41,8 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml index 1ff01cb879..dec6d21567 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml index a3e1e47e50..b64d28c87e 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml index 77324cb75d..71e12602c1 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml index aca9321b4c..37a8413d7c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml index 9aed4b026f..9952dfcbb8 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml index 17c2095906..2f1580db28 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml index d5a14ddf4d..dafc4a9a87 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml @@ -35,6 +35,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml index 626f1e21b3..c6f0067638 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 2 @@ -156,6 +158,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml index bf0b6dd7e2..b547225dbb 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml index 1657ffb134..f45ff6c0e8 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml index 67bfe4f61c..36c04783f2 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml index 14012bd254..c9fc5a7008 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml index f33326871d..0336533759 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml index 0584b59017..7fbbc63574 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml @@ -111,6 +111,8 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -151,6 +153,8 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true + reasoning-parser: qwen3 + tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 From ffe90545b29ad2c49f200ef4f344336fea6f31f1 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:07:40 -0500 Subject: [PATCH 31/99] fix: harden tool eval dispatch and artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:强化工具调用评估的分发流程与产物校验。 --- .github/workflows/e2e-tests.yml | 7 +- .github/workflows/run-sweep.yml | 2 +- benchmarks/benchmark_lib.sh | 19 +- benchmarks/multi_node/amd_utils/job.slurm | 7 + .../multi_node/amd_utils/server_atom.sh | 4 +- .../multi_node/amd_utils/server_sglang.sh | 4 +- .../multi_node/amd_utils/server_vllm.sh | 4 +- benchmarks/multi_node/amd_utils/submit.sh | 3 + benchmarks/multi_node/llm-d/job.slurm | 10 +- benchmarks/multi_node/llm-d/server.sh | 4 +- benchmarks/multi_node/llm-d/submit.sh | 4 + .../multi_node/tilert_utils/run_node.sh | 4 +- runners/launch_b200-dgxc.sh | 2 +- runners/launch_b200-nscale-slurm.sh | 5 + runners/launch_b300-nv.sh | 7 + runners/launch_gb200-nv.sh | 2 +- runners/launch_gb300-nv.sh | 7 + runners/launch_h100-cr.sh | 2 +- runners/launch_h100-dgxc-slurm.sh | 7 + runners/launch_h200-dgxc-slurm.sh | 2 +- runners/launch_mi325x-tw.sh | 2 +- runners/launch_rtx6000pro-lat.sh | 2 + runners/patch_srt_eval_dispatch.py | 9 + runners/synthetic_injectors/sglang.py | 25 +- runners/test_slurm_utils.py | 68 ++- utils/collect_eval_results.py | 49 +- utils/evals/test_run_eval_dispatch.py | 1 + utils/test_collect_eval_results.py | 102 +++- .../test_validate_reusable_sweep_artifacts.py | 570 +++++++++++++++++- utils/validate_reusable_sweep_artifacts.py | 569 +++++++++++++---- 30 files changed, 1355 insertions(+), 148 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f361b23144..978a690c5e 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -397,9 +397,11 @@ jobs: decode-additional-settings: ${{ toJson(matrix.config.decode.additional-settings) }} run-eval: true eval-only: true - eval-conc: ${{ matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} + eval-conc: ${{ inputs.eval-framework == 'lm-eval' && matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} + eval-limit: ${{ inputs.eval-limit }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} + swebench-gen-mode: ${{ inputs.swebench-gen-mode }} ref: ${{ inputs.ref }} test-sweep-agentic: @@ -571,7 +573,7 @@ jobs: precision: ${{ matrix.config.precision }} router: ${{ matrix.config.router && toJson(matrix.config.router) || '' }} kv-p2p-transfer: ${{ matrix.config['kv-p2p-transfer'] || '' }} - conc-list: ${{ toJson(matrix.config.conc) }} + conc-list: ${{ format('[{0}]', matrix.config['eval-conc']) }} spec-decoding: ${{ matrix.config.spec-decoding }} disagg: ${{ matrix.config.disagg }} prefill-hardware: ${{ matrix.config.prefill.hardware }} @@ -684,6 +686,7 @@ jobs: eval-limit: ${{ inputs.eval-limit }} eval-framework: ${{ inputs.eval-framework }} eval-suite: ${{ inputs.eval-suite }} + swebench-gen-mode: ${{ inputs.swebench-gen-mode }} ref: ${{ inputs.ref }} collect-results: diff --git a/.github/workflows/run-sweep.yml b/.github/workflows/run-sweep.yml index 62d013934b..ce5796522f 100644 --- a/.github/workflows/run-sweep.yml +++ b/.github/workflows/run-sweep.yml @@ -893,7 +893,7 @@ jobs: precision: ${{ matrix.config.precision }} router: ${{ matrix.config.router && toJson(matrix.config.router) || '' }} kv-p2p-transfer: ${{ matrix.config['kv-p2p-transfer'] || '' }} - conc-list: ${{ toJson(matrix.config.conc) }} + conc-list: ${{ format('[{0}]', matrix.config['eval-conc']) }} spec-decoding: ${{ matrix.config.spec-decoding }} disagg: ${{ matrix.config.disagg }} prefill-hardware: ${{ matrix.config.prefill.hardware }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 983f79465b..da1bf5df10 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -912,8 +912,8 @@ _prepare_kimi_vendor_python() { uv_prefix="${python_dir}/uv" uv_bin="${uv_prefix}/bin/uv" venv_dir="${python_dir}/venv" - python3 -m pip install -q --no-cache-dir --prefix "$uv_prefix" "uv==0.11.33" \ - || prepare_rc=$? + python3 -m pip install -q --no-cache-dir --break-system-packages \ + --prefix "$uv_prefix" "uv==0.11.33" || prepare_rc=$? if [ "$prepare_rc" -eq 0 ] && [ ! -x "$uv_bin" ]; then echo "ERROR: pinned uv installation did not create ${uv_bin}" >&2 prepare_rc=1 @@ -1254,6 +1254,7 @@ _run_kimi_tool_call_schema_eval() { local checkout_dir="" mkdir -p "$results_dir" || return $? + results_dir="$(cd "$results_dir" && pwd)" || return $? export EVAL_RESULT_DIR="$results_dir" local setup_rc=0 integration_error="" @@ -1693,7 +1694,11 @@ META } rewrite_lm_eval_meta_env() { - _write_lm_eval_meta_json "./meta_env.json" "" "${CONC:-1}" + if [ -n "${EVAL_BATCHED_CONCS:-}" ]; then + append_lm_eval_summary + else + _write_lm_eval_meta_json "./meta_env.json" "" "${CONC:-1}" + fi } append_lm_eval_summary() { @@ -2224,12 +2229,14 @@ run_eval() { *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac - if [ "$framework" = "kimi-vendor" ]; then + if [ -n "${EVAL_SUITE:-}" ]; then export EVAL_COMPLETED_SUITE="$EVAL_SUITE" fi - # Agentic eval-only recipes have no separate staging step. - if [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; then + # Agentic eval-only recipes have no separate staging step. Kimi failures + # also carry diagnostic score artifacts that callers must preserve. + if { [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; } \ + || { [ "$framework" = "kimi-vendor" ] && [ "$eval_rc" -ne 0 ]; }; then append_lm_eval_summary || true fi diff --git a/benchmarks/multi_node/amd_utils/job.slurm b/benchmarks/multi_node/amd_utils/job.slurm index 3c8a6f16a8..bed13098b7 100755 --- a/benchmarks/multi_node/amd_utils/job.slurm +++ b/benchmarks/multi_node/amd_utils/job.slurm @@ -323,6 +323,9 @@ export ENGINE=$ENGINE export RUN_EVAL="${RUN_EVAL:-false}" export EVAL_ONLY="${EVAL_ONLY:-false}" export EVAL_CONC="${EVAL_CONC:-}" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" +export EVAL_SUITE="${EVAL_SUITE:-}" +export SWEBENCH_GEN_MODE="${SWEBENCH_GEN_MODE:-}" export FRAMEWORK="${FRAMEWORK:-}" export PRECISION="${PRECISION:-}" export MODEL_PREFIX="${MODEL_PREFIX:-}" @@ -430,6 +433,10 @@ DOCKER_ENV_COMMON=( -e RUN_EVAL=\$RUN_EVAL -e EVAL_ONLY=\$EVAL_ONLY -e \"EVAL_CONC=\$EVAL_CONC\" + -e EVAL_FRAMEWORK=\$EVAL_FRAMEWORK + -e EVAL_LIMIT=\$EVAL_LIMIT + -e EVAL_SUITE=\$EVAL_SUITE + -e SWEBENCH_GEN_MODE=\$SWEBENCH_GEN_MODE -e FRAMEWORK=\$FRAMEWORK -e PRECISION=\$PRECISION -e MODEL_PREFIX=\$MODEL_PREFIX diff --git a/benchmarks/multi_node/amd_utils/server_atom.sh b/benchmarks/multi_node/amd_utils/server_atom.sh index 2fee5679ca..35483a3667 100755 --- a/benchmarks/multi_node/amd_utils/server_atom.sh +++ b/benchmarks/multi_node/amd_utils/server_atom.sh @@ -411,9 +411,9 @@ if [ "$NODE_RANK" -eq 0 ]; then fi if [[ "$DRY_RUN" -eq 1 ]]; then - echo "DRY RUN: run_eval --framework lm-eval --port ${ROUTER_PORT} (conc=${EVAL_CONCURRENT_REQUESTS})" + echo "DRY RUN: run_eval --port ${ROUTER_PORT} (framework=${EVAL_FRAMEWORK:-lm-eval}, conc=${EVAL_CONCURRENT_REQUESTS})" else - MODEL_NAME="${MODEL_DIR}/${MODEL_NAME}" run_eval --framework lm-eval --port ${ROUTER_PORT} + MODEL_NAME="${MODEL_DIR}/${MODEL_NAME}" run_eval --port "${ROUTER_PORT}" eval_rc=$? if [[ $eval_rc -ne 0 ]]; then diff --git a/benchmarks/multi_node/amd_utils/server_sglang.sh b/benchmarks/multi_node/amd_utils/server_sglang.sh index aaaca61ef5..45b5ca9647 100755 --- a/benchmarks/multi_node/amd_utils/server_sglang.sh +++ b/benchmarks/multi_node/amd_utils/server_sglang.sh @@ -1113,9 +1113,9 @@ print(json.dumps(json.loads(sys.stdin.read())))' <<<"$_val")" || { # RESULT_FILENAME are already set via Docker -e flags from job.slurm if [[ "$DRY_RUN" -eq 1 ]]; then - echo "DRY RUN: run_eval --framework lm-eval --port 30000 (conc=${EVAL_CONCURRENT_REQUESTS}, ctx=${EVAL_MAX_MODEL_LEN:-auto})" + echo "DRY RUN: run_eval --port 30000 (framework=${EVAL_FRAMEWORK:-lm-eval}, conc=${EVAL_CONCURRENT_REQUESTS}, ctx=${EVAL_MAX_MODEL_LEN:-auto})" else - run_eval --framework lm-eval --port 30000 + run_eval --port 30000 eval_rc=$? if [[ $eval_rc -ne 0 ]]; then diff --git a/benchmarks/multi_node/amd_utils/server_vllm.sh b/benchmarks/multi_node/amd_utils/server_vllm.sh index 55154cd015..c743444c1a 100755 --- a/benchmarks/multi_node/amd_utils/server_vllm.sh +++ b/benchmarks/multi_node/amd_utils/server_vllm.sh @@ -355,9 +355,9 @@ if [ "$NODE_RANK" -eq 0 ]; then fi if [[ "$DRY_RUN" -eq 1 ]]; then - echo "DRY RUN: run_eval --framework lm-eval --port $ROUTER_PORT (conc=${EVAL_CONCURRENT_REQUESTS}, ctx=${EVAL_MAX_MODEL_LEN:-auto})" + echo "DRY RUN: run_eval --port $ROUTER_PORT (framework=${EVAL_FRAMEWORK:-lm-eval}, conc=${EVAL_CONCURRENT_REQUESTS}, ctx=${EVAL_MAX_MODEL_LEN:-auto})" else - run_eval --framework lm-eval --port "$ROUTER_PORT" + run_eval --port "$ROUTER_PORT" eval_rc=$? if [[ $eval_rc -ne 0 ]]; then diff --git a/benchmarks/multi_node/amd_utils/submit.sh b/benchmarks/multi_node/amd_utils/submit.sh index 6a1598d81d..bbfaa2fce9 100755 --- a/benchmarks/multi_node/amd_utils/submit.sh +++ b/benchmarks/multi_node/amd_utils/submit.sh @@ -152,6 +152,9 @@ export DRY_RUN="${DRY_RUN:-0}" export RUN_EVAL="${RUN_EVAL:-false}" export EVAL_ONLY="${EVAL_ONLY:-false}" export EVAL_CONC="${EVAL_CONC:-}" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" +export EVAL_SUITE="${EVAL_SUITE:-}" +export SWEBENCH_GEN_MODE="${SWEBENCH_GEN_MODE:-}" export FRAMEWORK="${FRAMEWORK:-}" export PRECISION="${PRECISION:-}" export MODEL_PREFIX="${MODEL_PREFIX:-}" diff --git a/benchmarks/multi_node/llm-d/job.slurm b/benchmarks/multi_node/llm-d/job.slurm index f260c1a6fb..8d3a597731 100644 --- a/benchmarks/multi_node/llm-d/job.slurm +++ b/benchmarks/multi_node/llm-d/job.slurm @@ -150,6 +150,10 @@ exec docker run --rm \ -e RUN_EVAL=$RUN_EVAL \ -e EVAL_ONLY=$EVAL_ONLY \ -e EVAL_CONC=$EVAL_CONC \ + -e EVAL_FRAMEWORK=$EVAL_FRAMEWORK \ + -e EVAL_LIMIT=$EVAL_LIMIT \ + -e EVAL_SUITE=$EVAL_SUITE \ + -e SWEBENCH_GEN_MODE=$SWEBENCH_GEN_MODE \ -e FRAMEWORK=$FRAMEWORK \ -e PRECISION=$PRECISION \ -e MODEL_PREFIX=$MODEL_PREFIX \ @@ -186,7 +190,8 @@ elif [[ "$LLMD_CONTAINER_ENGINE" == "pyxis" ]]; then export PREFILL_DP_SIZE DECODE_DP_SIZE export BENCH_INPUT_LEN BENCH_OUTPUT_LEN BENCH_MAX_CONCURRENCY export BENCH_REQUEST_RATE BENCH_RANDOM_RANGE_RATIO BENCH_NUM_PROMPTS_MULTIPLIER - export RUN_EVAL EVAL_ONLY EVAL_CONC FRAMEWORK PRECISION MODEL_PREFIX + export RUN_EVAL EVAL_ONLY EVAL_CONC EVAL_FRAMEWORK EVAL_LIMIT EVAL_SUITE + export SWEBENCH_GEN_MODE FRAMEWORK PRECISION MODEL_PREFIX export RUNNER_TYPE RESULT_FILENAME SPEC_DECODING IS_MULTINODE CONFIG_FILE PYXIS_ENV_LIST="NUM_NODES,PREFILL_NODES,DECODE_NODES,ALL_IPS,PREFILL_LEADER_IP,DECODE_LEADER_IP" @@ -195,7 +200,8 @@ elif [[ "$LLMD_CONTAINER_ENGINE" == "pyxis" ]]; then PYXIS_ENV_LIST+=",PREFILL_DP_SIZE,DECODE_DP_SIZE" PYXIS_ENV_LIST+=",BENCH_INPUT_LEN,BENCH_OUTPUT_LEN,BENCH_MAX_CONCURRENCY" PYXIS_ENV_LIST+=",BENCH_REQUEST_RATE,BENCH_RANDOM_RANGE_RATIO,BENCH_NUM_PROMPTS_MULTIPLIER" - PYXIS_ENV_LIST+=",RUN_EVAL,EVAL_ONLY,EVAL_CONC,FRAMEWORK,PRECISION,MODEL_PREFIX" + PYXIS_ENV_LIST+=",RUN_EVAL,EVAL_ONLY,EVAL_CONC,EVAL_FRAMEWORK,EVAL_LIMIT,EVAL_SUITE" + PYXIS_ENV_LIST+=",SWEBENCH_GEN_MODE,FRAMEWORK,PRECISION,MODEL_PREFIX" PYXIS_ENV_LIST+=",RUNNER_TYPE,RESULT_FILENAME,SPEC_DECODING,IS_MULTINODE,CONFIG_FILE" PYXIS_MOUNTS="${MODEL_DIR}:/models:ro" diff --git a/benchmarks/multi_node/llm-d/server.sh b/benchmarks/multi_node/llm-d/server.sh index 7e189767bc..89894b210c 100755 --- a/benchmarks/multi_node/llm-d/server.sh +++ b/benchmarks/multi_node/llm-d/server.sh @@ -562,6 +562,7 @@ PY _bench_prefill_gpus=$(( PREFILL_NODES * GPUS_PER_NODE )) _bench_decode_gpus=$(( DECODE_NODES * GPUS_PER_NODE )) _bench_total_gpus=$(( _bench_prefill_gpus + _bench_decode_gpus )) + if [[ "${EVAL_ONLY:-false}" != "true" ]]; then for max_concurrency in "${CONCURRENCIES[@]}"; do num_prompts=$(( max_concurrency * BENCH_NUM_PROMPTS_MULTIPLIER )) [[ "$num_prompts" -lt 16 ]] && num_prompts=16 @@ -601,6 +602,7 @@ PY "${bench_extra_args[@]}" \ || echo "WARNING: benchmark conc=$max_concurrency failed/timed out (rc=$?)" done + fi # ---- Eval (optional) ---- if [[ "${RUN_EVAL:-false}" == "true" ]]; then @@ -624,7 +626,7 @@ PY # the host-side workflow checks look; the subshell keeps the cd local. ( cd /workspace - run_eval --framework lm-eval --port "$ENVOY_PORT" + run_eval --port "$ENVOY_PORT" append_lm_eval_summary ) fi diff --git a/benchmarks/multi_node/llm-d/submit.sh b/benchmarks/multi_node/llm-d/submit.sh index 11c34736f8..3781a52382 100755 --- a/benchmarks/multi_node/llm-d/submit.sh +++ b/benchmarks/multi_node/llm-d/submit.sh @@ -79,6 +79,10 @@ export BENCH_NUM_PROMPTS_MULTIPLIER="${BENCH_NUM_PROMPTS_MULTIPLIER:-10}" export RUN_EVAL="${RUN_EVAL:-false}" export EVAL_ONLY="${EVAL_ONLY:-false}" export EVAL_CONC="${EVAL_CONC:-}" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" +export EVAL_LIMIT="${EVAL_LIMIT:-}" +export EVAL_SUITE="${EVAL_SUITE:-}" +export SWEBENCH_GEN_MODE="${SWEBENCH_GEN_MODE:-}" export FRAMEWORK="${FRAMEWORK:-llmd-vllm}" export PRECISION="${PRECISION:-}" export MODEL_PREFIX="${MODEL_PREFIX:-}" diff --git a/benchmarks/multi_node/tilert_utils/run_node.sh b/benchmarks/multi_node/tilert_utils/run_node.sh index c6d3f0046c..4fde2f7ec5 100755 --- a/benchmarks/multi_node/tilert_utils/run_node.sh +++ b/benchmarks/multi_node/tilert_utils/run_node.sh @@ -196,6 +196,7 @@ run_bench_and_eval() { wait_for_server_ready --port "$ROUTER_PORT" \ --server-log "$BENCHMARK_LOGS_DIR/tilert_router.log" --server-pid "$ROUTER_PID" local rc=0 conc np + if [[ "${EVAL_ONLY:-false}" != "true" ]]; then for conc in $CONC_LIST; do np=$(( conc * 10 )) [[ "$np" -lt 16 ]] && np=16 @@ -211,6 +212,7 @@ run_bench_and_eval() { --result-filename "$(bench_result_stem "$conc")" --result-dir "$RESULT_DIR" \ || { rc=$?; echo "[bench] WARNING: conc=$conc failed/timed out (rc=$rc)"; } done + fi if [[ "${RUN_EVAL}" = "true" ]]; then if [[ -n "${EVAL_CONC:-}" ]]; then export EVAL_CONCURRENT_REQUESTS="$EVAL_CONC" @@ -218,7 +220,7 @@ run_bench_and_eval() { export EVAL_CONCURRENT_REQUESTS="$(tr ' ' '\n' <<< "$CONC_LIST" | sort -n | tail -1)" fi export CONC="$EVAL_CONCURRENT_REQUESTS" - run_eval --framework lm-eval --port "$ROUTER_PORT" + run_eval --port "$ROUTER_PORT" append_lm_eval_summary fi return $rc diff --git a/runners/launch_b200-dgxc.sh b/runners/launch_b200-dgxc.sh index 7758ad90ee..cef54f365a 100644 --- a/runners/launch_b200-dgxc.sh +++ b/runners/launch_b200-dgxc.sh @@ -220,7 +220,7 @@ if [[ "$IS_MULTINODE" == "true" ]]; then cd "$SRT_REPO_DIR" || exit 1 git checkout sa-submission-q2-2026 fi - if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then + if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" || exit 1 fi diff --git a/runners/launch_b200-nscale-slurm.sh b/runners/launch_b200-nscale-slurm.sh index 9b9a1e3f96..ade52d23a5 100755 --- a/runners/launch_b200-nscale-slurm.sh +++ b/runners/launch_b200-nscale-slurm.sh @@ -81,6 +81,11 @@ else mkdir -p recipes/vllm/kimi-k2.6 cp -rT "$GITHUB_WORKSPACE/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k2.6" recipes/vllm/kimi-k2.6 fi +if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" \ + || exit 1 +fi + echo "Installing srtctl..." export UV_INSTALL_DIR="$GITHUB_WORKSPACE/.local/bin" diff --git a/runners/launch_b300-nv.sh b/runners/launch_b300-nv.sh index 5fced02ffb..303c5fb3dd 100644 --- a/runners/launch_b300-nv.sh +++ b/runners/launch_b300-nv.sh @@ -115,6 +115,11 @@ else cd "$SRT_REPO_DIR" || exit 1 git checkout sa-submission-q2-2026 fi +if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" \ + || exit 1 +fi + echo "Installing srtctl..." export UV_INSTALL_DIR="$GITHUB_WORKSPACE/.local/bin" @@ -203,6 +208,8 @@ sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" if [[ "$MODEL_PREFIX" == "minimaxm3" && -n "$MINIMAX_M3_SLURM_EXCLUDED_NODELIST" ]]; then sed -i "/^name:.*/a sbatch_directives:\n exclude: \"${MINIMAX_M3_SLURM_EXCLUDED_NODELIST}\"" "$CONFIG_PATH" fi +python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 SRTCTL_APPLY_ARGS=( -f "$CONFIG_FILE" --tags "b300,${MODEL_PREFIX},${PRECISION},${ISL}x${OSL},infmax-$(date +%Y%m%d)" diff --git a/runners/launch_gb200-nv.sh b/runners/launch_gb200-nv.sh index c4c0e53847..483816c333 100755 --- a/runners/launch_gb200-nv.sh +++ b/runners/launch_gb200-nv.sh @@ -503,7 +503,7 @@ else git clone --branch cam/sa-submission-q2-2026 --single-branch https://github.com/cquil11/srt-slurm-nv.git "$SRT_REPO_DIR" cd "$SRT_REPO_DIR" fi -if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then +if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" || exit 1 fi diff --git a/runners/launch_gb300-nv.sh b/runners/launch_gb300-nv.sh index 27154c404e..2cd8a8525d 100644 --- a/runners/launch_gb300-nv.sh +++ b/runners/launch_gb300-nv.sh @@ -330,6 +330,11 @@ else git checkout sa-submission-q2-2026 fi +if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" \ + || exit 1 +fi + echo "Installing srtctl..." export UV_INSTALL_DIR="$GITHUB_WORKSPACE/.local/bin" curl -LsSf https://astral.sh/uv/install.sh | sh @@ -424,6 +429,8 @@ fi # below still receives the full CONFIG_FILE (with selector). CONFIG_PATH="${CONFIG_FILE%%:*}" sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" +python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 # --no-preflight skips srtctl's pre-submit model-path stat, which runs on # the GHA runner host (im-gb300-login-02, an x86 login node). It's required diff --git a/runners/launch_h100-cr.sh b/runners/launch_h100-cr.sh index e7bd48dfa9..f111a0dfcb 100644 --- a/runners/launch_h100-cr.sh +++ b/runners/launch_h100-cr.sh @@ -18,7 +18,7 @@ docker run --rm --network=host --name=$server_name \ --runtime=nvidia --gpus="$GPU_COUNT" --ipc=host --privileged --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 \ -v $HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE \ -v $GITHUB_WORKSPACE:/workspace/ -w /workspace/ \ --e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ +-e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e SWEBENCH_GEN_MODE -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ -e PROFILE -e SGLANG_TORCH_PROFILER_DIR -e VLLM_TORCH_PROFILER_DIR -e VLLM_RPC_TIMEOUT \ -e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e TORCH_CUDA_ARCH_LIST="9.0" -e CUDA_DEVICE_ORDER=PCI_BUS_ID -e CUDA_VISIBLE_DEVICES \ --entrypoint=/bin/bash \ diff --git a/runners/launch_h100-dgxc-slurm.sh b/runners/launch_h100-dgxc-slurm.sh index 1334c95542..cf619ab747 100644 --- a/runners/launch_h100-dgxc-slurm.sh +++ b/runners/launch_h100-dgxc-slurm.sh @@ -54,6 +54,11 @@ if [[ "$IS_MULTINODE" == "true" ]]; then cd "$SRT_REPO_DIR" git checkout sa-submission-q2-2026 fi + if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" \ + || exit 1 + fi + echo "Installing srtctl..." export UV_INSTALL_DIR="/mnt/nfs/sa-shared/.uv/bin" @@ -145,6 +150,8 @@ EOF sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_FILE" # Raise sglang's torch-distributed TCPStore timeout from the 600s gloo default sed -i '/^ watchdog-timeout:/a\ dist-timeout: 1800' "${CONFIG_FILE%%:*}" + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 SRTCTL_OUTPUT=$(srtctl apply -f "$CONFIG_FILE" --tags "h100,${MODEL_PREFIX},${PRECISION},${ISL}x${OSL},infmax-$(date +%Y%m%d)" 2>&1) echo "$SRTCTL_OUTPUT" diff --git a/runners/launch_h200-dgxc-slurm.sh b/runners/launch_h200-dgxc-slurm.sh index 61c290146e..130d4a98ae 100755 --- a/runners/launch_h200-dgxc-slurm.sh +++ b/runners/launch_h200-dgxc-slurm.sh @@ -106,7 +106,7 @@ if [[ "$IS_MULTINODE" == "true" ]]; then cd "$SRT_REPO_DIR" git checkout sa-submission-q2-2026 fi - if [[ "${EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor" ]]; then + if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" fi diff --git a/runners/launch_mi325x-tw.sh b/runners/launch_mi325x-tw.sh index 0ed4be196c..0b6e629760 100644 --- a/runners/launch_mi325x-tw.sh +++ b/runners/launch_mi325x-tw.sh @@ -26,7 +26,7 @@ docker run --rm --network=host --name="$server_name" \ --security-opt seccomp=unconfined --cap-add=SYS_PTRACE \ -v "$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE" \ -v "$GITHUB_WORKSPACE:/workspace/" -w /workspace/ \ --e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT="$PORT" \ +-e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e SWEBENCH_GEN_MODE -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT="$PORT" \ -e DP_ATTENTION -e EP_SIZE -e DP_SIZE -e EVAL_MAX_MODEL_LEN -e SPEC_DECODING -e NUM_SPEC_TOKENS \ -e PROFILE -e SGLANG_TORCH_PROFILER_DIR -e VLLM_TORCH_PROFILER_DIR -e VLLM_RPC_TIMEOUT \ -e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e CUDA_DEVICE_ORDER=PCI_BUS_ID \ diff --git a/runners/launch_rtx6000pro-lat.sh b/runners/launch_rtx6000pro-lat.sh index 6ad56e7092..4a63a446d7 100755 --- a/runners/launch_rtx6000pro-lat.sh +++ b/runners/launch_rtx6000pro-lat.sh @@ -106,7 +106,9 @@ docker run \ --env NUM_SPEC_TOKENS \ --env RUN_EVAL \ --env EVAL_ONLY \ + --env EVAL_FRAMEWORK \ --env EVAL_LIMIT \ + --env EVAL_SUITE \ --env EVAL_MAX_MODEL_LEN \ --env RUNNER_TYPE \ --env RUNNER_NAME \ diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index daa52d9ab0..8c1f9d5fd6 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -11,7 +11,9 @@ "IS_MULTINODE",""" DO_SWEEP_ENV_REPLACEMENT = """ "EVAL_ONLY", "EVAL_FRAMEWORK", + "EVAL_LIMIT", "EVAL_SUITE", + "SWEBENCH_GEN_MODE", "IS_MULTINODE",""" LM_EVAL_COMMAND = 'run_eval --framework lm-eval --port "$PORT" || eval_rc=$?' GENERIC_EVAL_COMMAND = 'run_eval --port "$PORT" || eval_rc=$?' @@ -34,6 +36,13 @@ def prepare_replacements( old_count = content.count(old) new_count = content.count(new) if old_count == 1 and new_count == 0: + anchor = old.splitlines()[0] + anchor_count = content.count(anchor) + if anchor_count != 1: + raise RuntimeError( + f"invalid patch state in {path}: " + f"anchor {anchor!r} count={anchor_count}" + ) content = content.replace(old, new, 1) changed = True elif old_count != 0 or new_count != 1: diff --git a/runners/synthetic_injectors/sglang.py b/runners/synthetic_injectors/sglang.py index 48fb1706aa..ed58144776 100644 --- a/runners/synthetic_injectors/sglang.py +++ b/runners/synthetic_injectors/sglang.py @@ -7,6 +7,9 @@ _SPEC_STEPS_RE = re.compile(r"(?m)^\s+speculative-num-steps:\s*([0-9]+)\s*$") _ENV_BLOCK_RE = re.compile(r"(?m)^( (?:aggregated|prefill|decode)_environment:\s*)$") +_SIMULATED_ACCEPTANCE_ENV_RE = re.compile( + r"(?m)^[ \t]+SGLANG_SIMULATE_ACC_(?:LEN|METHOD|TOKEN_MODE):[^\n]*(?:\n|$)" +) def spec_tokens_from_recipe(text): @@ -16,18 +19,30 @@ def spec_tokens_from_recipe(text): def rewrite(content, al, log): - """Add throughput-only golden-acceptance variables to each worker role.""" - if "SGLANG_SIMULATE_ACC_LEN" in content: - raise ValueError("recipe already contains SGLANG_SIMULATE_ACC_* variables") + """Set throughput-only golden acceptance in each worker role.""" + content, removed = _SIMULATED_ACCEPTANCE_ENV_RE.subn("", content) + if removed: + log(f"Replaced {removed} existing SGLANG_SIMULATE_ACC_* variable(s)") variables = ( f'\n SGLANG_SIMULATE_ACC_LEN: "{al:g}"' '\n SGLANG_SIMULATE_ACC_METHOD: "match-expected"' '\n SGLANG_SIMULATE_ACC_TOKEN_MODE: "real-draft-token"' ) - rewritten, count = _ENV_BLOCK_RE.subn(lambda match: match.group(1) + variables, content) + rewritten, count = _ENV_BLOCK_RE.subn( + lambda match: match.group(1) + variables, + content, + ) + if count: + log(f"Set SGLANG_SIMULATE_ACC_* in {count} worker environment block(s)") + return rewritten, count + + +def rewrite_real(content, log): + """Remove throughput-only simulated-acceptance variables for evals.""" + rewritten, count = _SIMULATED_ACCEPTANCE_ENV_RE.subn("", content) if count: - log(f"Added SGLANG_SIMULATE_ACC_* to {count} worker environment block(s)") + log(f"Removed {count} SGLANG_SIMULATE_ACC_* environment variable(s)") return rewritten, count diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index a89a3354bb..eb5bf45b94 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -99,6 +99,8 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert second.returncode == 0, second.stderr assert do_sweep.read_text().count('"EVAL_FRAMEWORK"') == 1 assert do_sweep.read_text().count('"EVAL_SUITE"') == 1 + assert do_sweep.read_text().count('"EVAL_LIMIT"') == 1 + assert do_sweep.read_text().count('"SWEBENCH_GEN_MODE"') == 1 assert 'run_eval --port "$PORT"' in eval_script.read_text() assert "--framework lm-eval" not in eval_script.read_text() assert "*_vendor_report.json" in eval_script.read_text() @@ -189,6 +191,64 @@ def test_eval_only_restores_real_vllm_acceptance(tmp_path: Path) -> None: assert "synthetic_acceptance_length" not in rewritten + +def test_eval_only_removes_sglang_simulated_acceptance(tmp_path: Path) -> None: + recipe = tmp_path / "recipe.yaml" + recipe.write_text( + "backend:\n" + " sglang_config:\n" + " decode_environment:\n" + ' SGLANG_SIMULATE_ACC_LEN: "2.99"\n' + ' SGLANG_SIMULATE_ACC_METHOD: "match-expected"\n' + ' SGLANG_SIMULATE_ACC_TOKEN_MODE: "real-draft-token"\n' + " KEEP_ME: unchanged\n" + ) + + result = subprocess.run( + ["python3", str(INJECT_ACCEPTANCE), str(recipe), "dynamo-sglang"], + env={**os.environ, "EVAL_ONLY": "true"}, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + rewritten = recipe.read_text() + assert "SGLANG_SIMULATE_ACC_" not in rewritten + assert "KEEP_ME: unchanged" in rewritten + + +def test_sglang_throughput_replaces_existing_simulated_acceptance( + tmp_path: Path, +) -> None: + recipe = tmp_path / "recipe.yaml" + recipe.write_text( + "backend:\n" + " aggregated_environment:\n" + ' SGLANG_SIMULATE_ACC_LEN: "2.99"\n' + ' SGLANG_SIMULATE_ACC_METHOD: "match-expected"\n' + ' SGLANG_SIMULATE_ACC_TOKEN_MODE: "real-draft-token"\n' + " KEEP_ME: unchanged\n" + ) + + result = subprocess.run( + ["python3", str(INJECT_ACCEPTANCE), str(recipe), "dynamo-sglang"], + env={ + **os.environ, + "SYNTHETIC_ACCEPTANCE": "true", + "SYNTHETIC_ACCEPTANCE_LENGTH": "3.39", + }, + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + rewritten = recipe.read_text() + assert rewritten.count("SGLANG_SIMULATE_ACC_LEN") == 1 + assert 'SGLANG_SIMULATE_ACC_LEN: "3.39"' in rewritten + assert "KEEP_ME: unchanged" in rewritten + def test_eval_only_acceptance_rewrite_allows_non_speculative_recipe( tmp_path: Path, ) -> None: @@ -210,16 +270,20 @@ def test_eval_only_acceptance_rewrite_allows_non_speculative_recipe( def test_nvidia_srt_launchers_prepare_kimi_eval_dispatch() -> None: launchers = ( + REPO_ROOT / "runners/launch_h100-dgxc-slurm.sh", REPO_ROOT / "runners/launch_h200-dgxc-slurm.sh", REPO_ROOT / "runners/launch_b200-dgxc.sh", + REPO_ROOT / "runners/launch_b200-nscale-slurm.sh", + REPO_ROOT / "runners/launch_b300-nv.sh", REPO_ROOT / "runners/launch_gb200-nv.sh", + REPO_ROOT / "runners/launch_gb300-nv.sh", ) for launcher in launchers: content = launcher.read_text() assert "patch_srt_eval_dispatch.py" in content - assert 'EVAL_FRAMEWORK:-lm-eval}" == "kimi-vendor"' in content - assert "inject_synthetic_acceptance.py" in content + assert 'EVAL_FRAMEWORK:-lm-eval}" != "lm-eval"' in content + assert "inject_synthetic_acceptance" in content def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 545ef3a855..881e6d3e14 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -3,6 +3,7 @@ import json import math import re +from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from tabulate import tabulate @@ -84,16 +85,33 @@ def detect_lm_eval_jsons(d: Path, batched: bool = False) -> List[Path]: ) lm_paths = [] - def recency_key(path: Path) -> Tuple[str, int, str]: + def recency_key(path: Path) -> Tuple[int, str]: match = re.search( r"\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d+)?", path.name, ) - return ( - match.group(0) if match else "", - path.stat().st_mtime_ns, - path.name, - ) + if match: + try: + timestamp = match.group(0) + base, separator, fraction = timestamp.partition(".") + parsed = datetime.strptime( + base, + "%Y-%m-%dT%H-%M-%S", + ).replace(tzinfo=timezone.utc) + fractional_ns = ( + int((fraction + "000000000")[:9]) + if separator + else 0 + ) + order_ns = ( + int(parsed.timestamp()) * 1_000_000_000 + + fractional_ns + ) + except ValueError: + order_ns = path.stat().st_mtime_ns + else: + order_ns = path.stat().st_mtime_ns + return order_ns, path.name for p in immediate_jsons: data = load_json(p) @@ -326,6 +344,10 @@ def build_row(meta: Dict[str, Any], m: Dict[str, Any]) -> Dict[str, Any]: row['score'] = m.get('accuracy') row['score_name'] = 'accuracy' row['score_se'] = m.get('accuracy_se') + elif m.get('flex') is not None: + row['score'] = m.get('flex') + row['score_name'] = 'em_flexible' + row['score_se'] = m.get('flex_se') else: row['score'] = None row['score_name'] = None @@ -359,6 +381,21 @@ def collect_eval_rows(root: Path) -> List[Dict[str, Any]]: metrics_list = extract_lm_metrics(lm_path) for metrics in metrics_list: + primary_score = next( + ( + metrics.get(name) + for name in ('strict', 'accuracy', 'flex') + if metrics.get(name) is not None + ), + None, + ) + if ( + isinstance(primary_score, bool) + or not isinstance(primary_score, (int, float)) + or not math.isfinite(primary_score) + or not 0.0 <= primary_score <= 1.0 + ): + continue rows.append(build_row(row_meta, metrics)) return rows diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index b3ab4ee8f1..7e47084428 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -634,6 +634,7 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( assert "VERSION_CHECK" in log assert "SYSTEM_PYTHON_ARG=<--prefix>" in log + assert "SYSTEM_PYTHON_ARG=<--break-system-packages>" in log assert "SYSTEM_PYTHON_ARG=" in log assert "UV_ARG=" in log assert "UV_ARG=<--python>" in log diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 13f1cefe2e..0521d03073 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -1,6 +1,7 @@ """Tests for eval result aggregation.""" import json +import os from pathlib import Path from collect_eval_results import ( @@ -231,4 +232,103 @@ def test_collect_eval_rows_does_not_resurrect_stale_valid_result( current_path.touch() stale_path.touch() - assert collect_eval_rows(tmp_path) == [] \ No newline at end of file + assert collect_eval_rows(tmp_path) == [] + + +def test_collect_eval_rows_uses_mtime_for_newer_legacy_name( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_retry" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "kimi_tool_call_schema"}) + ) + stale_path = ( + artifact_dir / "results_kimi_vendor_2026-08-12T01-00-00.000000.json" + ) + _write_lm_eval_result(stale_path, 1.0, task="kimi_tool_call_schema") + current_path = artifact_dir / "results.json" + _write_lm_eval_result(current_path, 0.0, task="kimi_tool_call_schema") + current = json.loads(current_path.read_text()) + current["integration_error"] = { + "type": "RuntimeError", + "message": "latest attempt failed", + } + current_path.write_text(json.dumps(current)) + os.utime(current_path, (2_000_000_000, 2_000_000_000)) + + assert collect_eval_rows(tmp_path) == [] + + +def test_collect_eval_rows_rejects_missing_or_out_of_range_scores( + tmp_path: Path, +) -> None: + for index, score in enumerate( + (None, True, float("nan"), float("inf"), -0.1, 1.1) + ): + artifact_dir = tmp_path / f"eval_invalid_{index}" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "kimi_tool_call_schema"}) + ) + _write_lm_eval_result( + artifact_dir / f"results_{index}.json", + score, + task="kimi_tool_call_schema", + ) + + assert collect_eval_rows(tmp_path) == [] + + +def test_collect_eval_rows_falls_back_for_invalid_filename_timestamp( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_invalid_timestamp" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "kimi_tool_call_schema"}) + ) + _write_lm_eval_result( + artifact_dir / "results_2026-99-99T99-99-99.json", + 1.0, + task="kimi_tool_call_schema", + ) + + rows = collect_eval_rows(tmp_path) + + assert len(rows) == 1 + assert rows[0]["score"] == 1.0 + + +def test_collect_eval_rows_uses_extract_filter_as_primary_score( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_gpqa" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "gpqa_diamond_cot_n_shot"}) + ) + (artifact_dir / "results_gpqa.json").write_text(json.dumps({ + "lm_eval_version": "0.4.0", + "results": { + "gpqa_diamond_cot_n_shot": { + "exact_match,extract_abcd": 0.75, + "exact_match_stderr,extract_abcd": 0.02, + }, + }, + "configs": { + "gpqa_diamond_cot_n_shot": { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "extract_abcd"}], + }, + }, + "n-samples": { + "gpqa_diamond_cot_n_shot": {"effective": 8}, + }, + })) + + rows = collect_eval_rows(tmp_path) + + assert len(rows) == 1 + assert rows[0]["score"] == 0.75 + assert rows[0]["score_name"] == "em_flexible" \ No newline at end of file diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index cfa0a1df1a..bfcc6e5f0b 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import sys from pathlib import Path @@ -69,6 +70,30 @@ def single_eval_meta( return row +def raw_eval_result( + score: float = 0.9, + *, + effective: object = 10, + task: str = "gsm8k", +) -> dict: + return { + "lm_eval_version": "0.4.0", + "results": { + task: { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.01, + }, + }, + "configs": { + task: { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], + }, + }, + "n-samples": {task: {"effective": effective}}, + } + + def write_raw_eval_artifact( root: Path, conc: int, @@ -92,6 +117,9 @@ def write_raw_eval_artifact( ) ) ) + (artifact_dir / "results_test.json").write_text( + json.dumps(raw_eval_result()) + ) def multinode_eval_result(conc: int) -> dict: @@ -126,16 +154,19 @@ def write_raw_batched_eval_artifact( ) -> None: artifact_dir = root / "eval_gptoss_8k1k_batch" artifact_dir.mkdir() + completed = concs if completed_concs is None else completed_concs meta = multinode_eval_result(concs[0]) meta["infmax_model_prefix"] = meta.pop("model_prefix") meta["eval_concs"] = concs - meta["completed_eval_concs"] = ( - concs if completed_concs is None else completed_concs - ) + meta["completed_eval_concs"] = completed meta["failed_eval_concs"] = ( [] if failed_concs is None else failed_concs ) (artifact_dir / "meta_env.json").write_text(json.dumps(meta)) + for conc in completed: + (artifact_dir / f"results_test_conc{conc}.json").write_text( + json.dumps(raw_eval_result()) + ) def fixed_result(conc: int) -> dict: @@ -460,7 +491,24 @@ def test_eval_validation_expands_one_batched_multinode_artifact( assert validate_eval_artifacts(tmp_path) == [] -def test_eval_validation_accepts_completed_points_from_failed_batch( +def test_eval_validation_accepts_legacy_batch_without_failed_list( + tmp_path: Path, +) -> None: + concs = [4, 16] + write_eval_aggregate( + tmp_path, + [multinode_eval_result(conc) for conc in concs], + ) + write_raw_batched_eval_artifact(tmp_path, concs) + meta_path = tmp_path / "eval_gptoss_8k1k_batch" / "meta_env.json" + meta = json.loads(meta_path.read_text()) + meta.pop("failed_eval_concs") + meta_path.write_text(json.dumps(meta)) + + assert validate_eval_artifacts(tmp_path) == [] + + +def test_eval_validation_rejects_failed_batch( tmp_path: Path, ) -> None: requested_concs = [4, 16, 64] @@ -476,7 +524,9 @@ def test_eval_validation_accepts_completed_points_from_failed_batch( failed_concs=[16], ) - assert validate_eval_artifacts(tmp_path) == [] + errors = validate_eval_artifacts(tmp_path) + + assert any("reports failed eval concurrencies" in error for error in errors) def test_eval_aggregate_validation_is_exact(tmp_path: Path) -> None: @@ -511,6 +561,150 @@ def test_eval_aggregate_validation_rejects_duplicate_identity( ) +def test_eval_aggregate_validation_rejects_non_list_file( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32) + eval_dir = tmp_path / "eval_results_all" + eval_dir.mkdir() + (eval_dir / "agg_eval_all.json").write_text( + json.dumps(single_eval_result(32)) + ) + + errors = validate_eval_artifacts(tmp_path) + + assert any("is not a list" in error for error in errors) + + +def test_eval_aggregate_validation_rejects_non_object_row( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32) + eval_dir = tmp_path / "eval_results_all" + eval_dir.mkdir() + (eval_dir / "agg_eval_all.json").write_text( + json.dumps([single_eval_result(32), "not-a-row"]) + ) + + errors = validate_eval_artifacts(tmp_path) + + assert any("row 1 is not an object" in error for error in errors) + + +def test_eval_validation_rejects_scores_outside_unit_interval( + tmp_path: Path, +) -> None: + for index, score in enumerate((-0.01, 1.01)): + root = tmp_path / str(index) + root.mkdir() + write_raw_eval_artifact(root, 32) + result_path = next( + (root / "eval_result_conc32_h100-dgxc-slurm_00").glob( + "results*.json" + ) + ) + result_path.write_text(json.dumps(raw_eval_result(score))) + write_eval_aggregate(root, [single_eval_result(32)]) + + errors = validate_eval_artifacts(root) + + assert any("invalid score" in error for error in errors) + + +def test_eval_validation_rejects_directory_with_only_markerless_results( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32) + result_path = next( + ( + tmp_path / "eval_result_conc32_h100-dgxc-slurm_00" + ).glob("results*.json") + ) + data = json.loads(result_path.read_text()) + data.pop("lm_eval_version") + result_path.write_text(json.dumps(data)) + write_eval_aggregate(tmp_path, [single_eval_result(32)]) + + errors = validate_eval_artifacts(tmp_path) + + assert any("has no recognized eval result" in error for error in errors) + + +def test_eval_validation_accepts_neutral_result_format_marker( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32) + result_path = next( + ( + tmp_path / "eval_result_conc32_h100-dgxc-slurm_00" + ).glob("results*.json") + ) + data = json.loads(result_path.read_text()) + data.pop("lm_eval_version") + data["result_format"] = "inferencex-eval-v1" + result_path.write_text(json.dumps(data)) + write_eval_aggregate(tmp_path, [single_eval_result(32)]) + + assert validate_eval_artifacts(tmp_path) == [] + + +def test_eval_validation_rejects_invalid_legacy_concurrency( + tmp_path: Path, +) -> None: + for index, conc in enumerate((0, -1, True, "4")): + root = tmp_path / str(index) + root.mkdir() + artifact_dir = root / "eval_invalid_legacy" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({**single_eval_meta(4), "conc": conc}) + ) + (artifact_dir / "results_test.json").write_text( + json.dumps(raw_eval_result()) + ) + + errors = validate_eval_artifacts(root) + + assert any("invalid legacy concurrency" in error for error in errors) + + +def test_eval_validation_rejects_malformed_batch_metadata( + tmp_path: Path, +) -> None: + cases = ( + ({"eval_concs": [4, True]}, "invalid eval_concs"), + ({"eval_concs": [4, 4]}, "duplicate eval_concs"), + ({"completed_eval_concs": [4, 4]}, "duplicate completed_eval_concs"), + ({"failed_eval_concs": [16, 16]}, "duplicate failed_eval_concs"), + ({"completed_eval_concs": [4, 16], "failed_eval_concs": [16]}, "overlapping"), + ({"completed_eval_concs": [4, 32]}, "unexpected"), + ({"completed_eval_concs": [4], "failed_eval_concs": [32]}, "failed unexpected"), + ({"completed_eval_concs": "4"}, "invalid batched concurrency metadata"), + ({"completed_eval_concs": [4], "failed_eval_concs": [16]}, "reports failed"), + ({"failed_eval_concs": None}, "invalid batched concurrency metadata"), + ({"eval_concs": [], "completed_eval_concs": []}, "empty eval_concs"), + ) + for index, (overrides, expected) in enumerate(cases): + root = tmp_path / str(index) + root.mkdir() + artifact_dir = root / "eval_invalid_batch" + artifact_dir.mkdir() + meta = _dd_meta(0) + meta.update( + { + "eval_concs": [4, 16], + "completed_eval_concs": [4, 16], + "failed_eval_concs": [], + } + ) + meta.update(overrides) + (artifact_dir / "meta_env.json").write_text(json.dumps(meta)) + + errors = validate_eval_artifacts(root) + + assert any(expected in error for error in errors), errors + + def test_fixed_sequence_validation_accepts_unique_source_rows(tmp_path: Path) -> None: results = tmp_path / "results_bmk" results.mkdir() @@ -679,7 +873,9 @@ def _dd_write_legacy_raw( artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text(json.dumps(_dd_meta(conc))) if timestamp is not None: - (artifact_dir / f"results_{timestamp}.json").write_text("{}") + (artifact_dir / f"results_{timestamp}.json").write_text( + json.dumps(raw_eval_result()) + ) def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: @@ -757,9 +953,12 @@ def test_dedupe_prunes_superseded_batched_conc(tmp_path: Path) -> None: meta = _dd_meta(0) meta["eval_concs"] = concs meta["completed_eval_concs"] = list(concs) + meta["failed_eval_concs"] = [] (artifact_dir / "meta_env.json").write_text(json.dumps(meta)) for conc in concs: - (artifact_dir / f"results_{stamp}_conc{conc}.json").write_text("{}") + (artifact_dir / f"results_{stamp}_conc{conc}.json").write_text( + json.dumps(raw_eval_result()) + ) _dd_write_aggregate( tmp_path, [ @@ -774,7 +973,364 @@ def test_dedupe_prunes_superseded_batched_conc(tmp_path: Path) -> None: assert validate_eval_artifacts(tmp_path) == [] assert json.loads((older / "meta_env.json").read_text())["completed_eval_concs"] == [16] + assert json.loads((older / "meta_env.json").read_text())["eval_concs"] == [16] assert not (older / "results_2026-06-26T10-00-00.000000_conc32.json").exists() assert (older / "results_2026-06-26T10-00-00.000000_conc16.json").exists() rows = json.loads((tmp_path / "eval_results_all" / "agg_eval_all.json").read_text()) assert [r["em_strict"] for r in rows if r["conc"] == 32] == [0.90] + + +def test_dedupe_orders_timestamped_and_legacy_results_coherently( + tmp_path: Path, +) -> None: + timestamped = "eval_minimaxm3_conc4096_b300-nv_timestamped" + legacy = "eval_minimaxm3_conc4096_b300-nv_legacy" + _dd_write_legacy_raw( + tmp_path, + timestamped, + 4096, + "2026-06-27T04-28-31.838775", + ) + _dd_write_legacy_raw(tmp_path, legacy, 4096, "retry") + legacy_result = next((tmp_path / legacy).glob("results*.json")) + future_ns = 2_000_000_000_000_000_000 + os.utime(legacy_result, ns=(future_ns, future_ns)) + _dd_write_aggregate( + tmp_path, + [ + _dd_agg_row( + 4096, + f"eval_results/{timestamped}/results_2026-06-27T04-28-31.838775.json", + 0.5, + ), + _dd_agg_row( + 4096, + f"eval_results/{legacy}/results_retry.json", + 0.9, + ), + ], + ) + + dedupe_reran_evals(tmp_path) + + assert validate_eval_artifacts(tmp_path) == [] + assert (tmp_path / legacy).is_dir() + assert not (tmp_path / timestamped).exists() + rows = json.loads( + (tmp_path / "eval_results_all" / "agg_eval_all.json").read_text() + ) + assert [row["em_strict"] for row in rows] == [0.9] + + +def test_dedupe_collapses_identity_across_aggregate_files( + tmp_path: Path, +) -> None: + old = "eval_minimaxm3_conc4096_b300-nv_old" + new = "eval_minimaxm3_conc4096_b300-nv_new" + _dd_write_legacy_raw( + tmp_path, + old, + 4096, + "2026-06-26T01-00-00.000000", + ) + _dd_write_legacy_raw( + tmp_path, + new, + 4096, + "2026-06-27T01-00-00.000000", + ) + aggregate_dir = tmp_path / "eval_results_all" + aggregate_dir.mkdir() + old_path = aggregate_dir / "old.json" + new_path = aggregate_dir / "new.json" + old_path.write_text( + json.dumps( + [ + _dd_agg_row( + 4096, + f"eval_results/{old}/results_2026-06-26T01-00-00.000000.json", + 0.4, + ) + ] + ) + ) + new_path.write_text( + json.dumps( + [ + _dd_agg_row( + 4096, + f"eval_results/{new}/results_2026-06-27T01-00-00.000000.json", + 0.9, + ) + ] + ) + ) + + messages = dedupe_reran_evals(tmp_path) + + assert json.loads(old_path.read_text()) == [] + assert [ + row["em_strict"] + for row in json.loads(new_path.read_text()) + ] == [0.9] + assert not (tmp_path / old).exists() + assert (tmp_path / new).is_dir() + assert validate_eval_artifacts(tmp_path) == [] + assert any("old.json: kept 0 of 1" in message for message in messages) + + +def test_dedupe_accepts_zero_score_as_structurally_valid( + tmp_path: Path, +) -> None: + old = "eval_minimaxm3_conc4096_b300-nv_old" + new = "eval_minimaxm3_conc4096_b300-nv_new" + _dd_write_legacy_raw(tmp_path, old, 4096, "2026-06-26T01-00-00.000000") + _dd_write_legacy_raw(tmp_path, new, 4096, "2026-06-27T01-00-00.000000") + new_result = next((tmp_path / new).glob("results*.json")) + new_result.write_text(json.dumps(raw_eval_result(0.0))) + _dd_write_aggregate( + tmp_path, + [ + _dd_agg_row(4096, f"eval_results/{old}/results_2026-06-26T01-00-00.000000.json", 0.8), + _dd_agg_row(4096, f"eval_results/{new}/results_2026-06-27T01-00-00.000000.json", 0.0), + ], + ) + + dedupe_reran_evals(tmp_path) + + assert validate_eval_artifacts(tmp_path) == [] + assert (tmp_path / new).is_dir() + assert not (tmp_path / old).exists() + + +def test_dedupe_does_not_resurrect_stale_result_after_integration_error( + tmp_path: Path, +) -> None: + name = "eval_minimaxm3_conc4096_b300-nv_retry" + _dd_write_legacy_raw( + tmp_path, + name, + 4096, + "2026-06-26T01-00-00.000000", + ) + new_result = ( + tmp_path + / name + / "results_2026-06-27T01-00-00.000000.json" + ) + failed = raw_eval_result(0.0) + failed["integration_error"] = { + "type": "RuntimeError", + "message": "verifier checkout failed", + } + new_result.write_text(json.dumps(failed)) + _dd_write_aggregate( + tmp_path, + [_dd_agg_row(4096, f"eval_results/{name}/results_old.json", 0.8)], + ) + + assert dedupe_reran_evals(tmp_path) == [] + errors = validate_eval_artifacts(tmp_path) + + assert any("integration error" in error for error in errors) + assert (tmp_path / name).is_dir() + + +def test_newer_foreign_result_does_not_suppress_recognized_result( + tmp_path: Path, +) -> None: + markerless = raw_eval_result() + markerless.pop("lm_eval_version") + for index, contents in enumerate(("{not-json", json.dumps(markerless))): + root = tmp_path / str(index) + root.mkdir() + name = "eval_minimaxm3_conc4096_b300-nv_retry" + _dd_write_legacy_raw( + root, + name, + 4096, + "2026-06-26T01-00-00.000000", + ) + ( + root + / name + / "results_2026-06-27T01-00-00.000000.json" + ).write_text(contents) + _dd_write_aggregate( + root, + [_dd_agg_row(4096, f"eval_results/{name}/results_old.json", 0.8)], + ) + + assert dedupe_reran_evals(root) == [] + assert validate_eval_artifacts(root) == [] + + +def test_dedupe_does_not_prune_when_latest_recognized_result_is_invalid( + tmp_path: Path, +) -> None: + cases = ( + (json.dumps({**raw_eval_result(), "results": {}}), "empty or malformed"), + (json.dumps({**raw_eval_result(), "results": []}), "empty or malformed"), + ( + json.dumps( + { + **raw_eval_result(), + "results": { + "gsm8k": {"exact_match,strict-match": "invalid"} + }, + } + ), + "invalid score", + ), + ( + json.dumps( + { + **raw_eval_result(), + "results": {"gsm8k": {"alias": "gsm8k"}}, + } + ), + "no score", + ), + (json.dumps(raw_eval_result(effective=0)), "invalid effective sample count"), + ( + json.dumps(raw_eval_result(effective="unknown")), + "invalid effective sample count", + ), + (json.dumps(raw_eval_result(effective=True)), "invalid effective sample count"), + ( + json.dumps( + { + **raw_eval_result(), + "n-samples": {"gsm8k": {}}, + } + ), + "malformed effective sample count", + ), + ) + for index, (contents, expected) in enumerate(cases): + root = tmp_path / str(index) + root.mkdir() + name = "eval_minimaxm3_conc4096_b300-nv_retry" + _dd_write_legacy_raw( + root, + name, + 4096, + "2026-06-26T01-00-00.000000", + ) + ( + root + / name + / "results_2026-06-27T01-00-00.000000.json" + ).write_text(contents) + _dd_write_aggregate( + root, + [_dd_agg_row(4096, f"eval_results/{name}/results_old.json", 0.8)], + ) + + assert dedupe_reran_evals(root) == [] + errors = validate_eval_artifacts(root) + + assert any(expected in error for error in errors), errors + assert (root / name).is_dir() + + +def test_dedupe_requires_aggregate_row_for_latest_raw_directory( + tmp_path: Path, +) -> None: + old = "eval_minimaxm3_conc4096_b300-nv_old" + new = "eval_minimaxm3_conc4096_b300-nv_new" + _dd_write_legacy_raw(tmp_path, old, 4096, "2026-06-26T01-00-00.000000") + _dd_write_legacy_raw(tmp_path, new, 4096, "2026-06-27T01-00-00.000000") + agg_path = _dd_write_aggregate( + tmp_path, + [ + _dd_agg_row( + 4096, + f"eval_results/{new}-unrelated/results_old.json", + 0.8, + ) + ], + ) + before = agg_path.read_text() + + assert dedupe_reran_evals(tmp_path) == [] + assert agg_path.read_text() == before + assert (tmp_path / old).is_dir() + assert (tmp_path / new).is_dir() + assert any("duplicate" in error for error in validate_eval_artifacts(tmp_path)) + + + +def test_eval_validation_accepts_extract_filter_primary_score( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32, eval_suite="gpqa") + raw_path = next(tmp_path.glob("eval_*/results*.json")) + data = raw_eval_result(task="gpqa") + data["results"]["gpqa"] = { + "exact_match,extract_abcd": 0.75, + "exact_match_stderr,extract_abcd": 0.02, + "answer_token_count": 42, + } + data["configs"]["gpqa"]["filter_list"] = [{"name": "extract_abcd"}] + raw_path.write_text(json.dumps(data)) + write_eval_aggregate( + tmp_path, + [single_eval_result(32, eval_suite="gpqa")], + ) + + assert validate_eval_artifacts(tmp_path) == [] + + +def test_eval_dedupe_leaves_invalid_suite_for_validation( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32) + raw_dir = next(tmp_path.glob("eval_*")) + meta_path = raw_dir / "meta_env.json" + meta = json.loads(meta_path.read_text()) + meta["eval_suite"] = [] + meta_path.write_text(json.dumps(meta)) + row = single_eval_result(32) + row["eval_suite"] = [] + write_eval_aggregate(tmp_path, [row]) + + assert dedupe_reran_evals(tmp_path) == [] + errors = validate_eval_artifacts(tmp_path) + assert any("invalid eval_suite" in error for error in errors) + + +def test_dedupe_uses_winning_legacy_result_mtime_for_aggregate( + tmp_path: Path, +) -> None: + artifact_name = "eval_minimaxm3_conc4096_b300-nv_retry" + _dd_write_legacy_raw(tmp_path, artifact_name, 4096, "a") + artifact_dir = tmp_path / artifact_name + older = artifact_dir / "results_b.json" + older.write_text(json.dumps(raw_eval_result())) + newer = artifact_dir / "results_a.json" + os.utime(older, ns=(1_000_000_000, 1_000_000_000)) + os.utime(newer, ns=(2_000_000_000, 2_000_000_000)) + _dd_write_aggregate( + tmp_path, + [ + _dd_agg_row( + 4096, + f"eval_results/{artifact_name}/results_b.json", + 0.1, + ), + _dd_agg_row( + 4096, + f"eval_results/{artifact_name}/results_a.json", + 0.9, + ), + ], + ) + + dedupe_reran_evals(tmp_path) + + rows = json.loads( + (tmp_path / "eval_results_all" / "agg_eval_all.json").read_text() + ) + assert [row["em_strict"] for row in rows] == [0.9] + assert validate_eval_artifacts(tmp_path) == [] \ No newline at end of file diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 8942d88fbf..4626ace73a 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -5,9 +5,11 @@ import argparse import json +import math import re import shutil import sys +from datetime import datetime, timezone from collections import Counter from pathlib import Path from typing import Any, Iterable, Optional @@ -314,6 +316,14 @@ def normalized_runner(value: Any) -> str: LEGACY_EVAL_SUITE = "" +def invalid_eval_suite(row: dict[str, Any]) -> bool: + """Return whether an explicit eval-suite identity is malformed.""" + suite = row.get("eval_suite") + return "eval_suite" in row and ( + not isinstance(suite, str) or not suite + ) + + @@ -379,10 +389,79 @@ def raw_eval_artifact_dirs(artifacts_dir: Path) -> list[Path]: ) +def _positive_int(value: Any) -> bool: + """Return whether value is a positive JSON integer (not a boolean).""" + return isinstance(value, int) and not isinstance(value, bool) and value > 0 + + +def _raw_meta_contributions( + artifact_name: str, + meta: dict[str, Any], +) -> tuple[ + list[tuple[tuple[Any, ...], Optional[int]]], + bool, + list[str], +]: + """Validate raw eval metadata and return its logical contributions.""" + prefix = f"raw eval artifact {artifact_name!r}" + if "eval_concs" not in meta: + conc = meta.get("conc") + if not _positive_int(conc): + return [], False, [f"{prefix} has invalid legacy concurrency"] + return [(eval_key(meta), None)], False, [] + + expected = meta.get("eval_concs") + completed = meta.get("completed_eval_concs") + failed = meta.get("failed_eval_concs", []) + fields = ( + ("eval_concs", expected), + ("completed_eval_concs", completed), + ("failed_eval_concs", failed), + ) + errors: list[str] = [] + if not all(isinstance(values, list) for _, values in fields): + return [], True, [f"{prefix} has invalid batched concurrency metadata"] + + for field, values in fields: + if any(not _positive_int(value) for value in values): + errors.append(f"{prefix} has invalid {field}") + continue + if len(set(values)) != len(values): + errors.append(f"{prefix} has duplicate {field}") + if errors: + return [], True, errors + + expected_set = set(expected) + completed_set = set(completed) + failed_set = set(failed) + if not expected_set: + errors.append(f"{prefix} has empty eval_concs") + if not completed_set: + errors.append(f"{prefix} has no completed eval concurrencies") + if not completed_set <= expected_set: + errors.append(f"{prefix} completed unexpected eval concurrencies") + if not failed_set <= expected_set: + errors.append(f"{prefix} failed unexpected eval concurrencies") + if completed_set & failed_set: + errors.append(f"{prefix} has overlapping completed and failed concurrencies") + if completed_set | failed_set != expected_set: + errors.append(f"{prefix} has unaccounted eval concurrencies") + if failed_set: + errors.append(f"{prefix} reports failed eval concurrencies") + if errors: + return [], True, errors + + return ( + [(eval_key({**meta, "conc": conc}), conc) for conc in completed], + True, + [], + ) + + def raw_eval_key_rows( artifacts_dir: Path, ) -> tuple[list[tuple[Any, ...]], list[str]]: - """Build logical eval identities from each raw artifact's metadata.""" + """Build and validate logical identities from raw eval artifacts.""" rows: list[tuple[Any, ...]] = [] errors: list[str] = [] for artifact_dir in raw_eval_artifact_dirs(artifacts_dir): @@ -394,7 +473,7 @@ def raw_eval_key_rows( continue try: meta = load_json(meta_path) - except (OSError, json.JSONDecodeError) as exc: + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: errors.append( f"raw eval artifact {artifact_dir.name!r} has invalid " f"meta_env.json: {exc}" @@ -406,20 +485,60 @@ def raw_eval_key_rows( "meta_env.json" ) continue - eval_concs = meta.get("completed_eval_concs") - if isinstance(meta.get("eval_concs"), list): - if not isinstance(eval_concs, list): + if invalid_eval_suite(meta): + errors.append( + f"raw eval artifact {artifact_dir.name!r} has invalid " + "eval_suite" + ) + continue + + contributions, batched, meta_errors = _raw_meta_contributions( + artifact_dir.name, + meta, + ) + errors.extend(meta_errors) + if meta_errors: + continue + rows.extend(key for key, _ in contributions) + + result_paths = _recognized_eval_result_paths( + artifact_dir.glob("results*.json") + ) + if batched: + expected = set(meta["eval_concs"]) + for path in result_paths: + conc = _result_concurrency(path.name) + if conc is None: + errors.append( + f"raw eval artifact {artifact_dir.name!r} has batched " + f"result {path.name!r} without a concurrency suffix" + ) + elif conc not in expected: + errors.append( + f"raw eval artifact {artifact_dir.name!r} has result " + f"{path.name!r} for unexpected concurrency {conc}" + ) + + for _, conc in contributions: + candidates = [ + path + for path in result_paths + if not batched or _result_concurrency(path.name) == conc + ] + conc_label = f" for concurrency {conc}" if conc is not None else "" + if not candidates: errors.append( - f"raw eval artifact {artifact_dir.name!r} has invalid " - "batched concurrency metadata" + f"raw eval artifact {artifact_dir.name!r} has no " + f"recognized eval result{conc_label}" ) continue - rows.extend( - eval_key({**meta, "conc": eval_conc}) - for eval_conc in eval_concs - ) - else: - rows.append(eval_key(meta)) + latest = max(candidates, key=_result_order) + result_error = _raw_result_error(latest) + if result_error is not None: + errors.append( + f"raw eval artifact {artifact_dir.name!r} latest result " + f"{latest.name!r}{conc_label} {result_error}" + ) return rows, errors @@ -439,14 +558,33 @@ def validate_eval_artifacts( row_count = 0 aggregate_rows: list[tuple[Any, ...]] = [] for path in aggregate_files: - data = load_json(path) - if isinstance(data, list): - row_count += len(data) - aggregate_rows.extend( - eval_key(row) - for row in data - if isinstance(row, dict) + try: + data = load_json(path) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + errors.append( + f"eval aggregate {path.name!r} is invalid JSON: {exc}" ) + continue + if not isinstance(data, list): + errors.append( + f"eval aggregate {path.name!r} is not a list" + ) + continue + row_count += len(data) + for index, row in enumerate(data): + if not isinstance(row, dict): + errors.append( + f"eval aggregate {path.name!r} row {index} " + "is not an object" + ) + continue + if invalid_eval_suite(row): + errors.append( + f"eval aggregate {path.name!r} row {index} " + "has invalid eval_suite" + ) + continue + aggregate_rows.append(eval_key(row)) if row_count == 0: errors.append("eval_results_all contains no rows") errors.extend( @@ -480,16 +618,17 @@ def validate_run_stats(artifacts_dir: Path, required: bool) -> list[str]: # A flaky eval retried several times leaves multiple raw ``eval_*`` dirs and # multiple ``eval_results_all`` rows for one logical eval identity, which the # checks above would otherwise reject. ``dedupe_reran_evals`` collapses those to -# the latest result per identity (by lm-eval result timestamp) so a legitimate +# the latest result per identity (by timestamp or legacy mtime) so a legitimate # rerun does not fail validation. It only acts on identities that have a clear -# latest result; genuinely ambiguous duplicates (no result timestamp to order -# them by) are left in place for validation to reject. Eval-only; fixed-sequence -# and agentic artifacts are untouched. +# latest result, ordered by a filename timestamp or legacy mtime. Identities +# with no result file are left in place for validation to reject. Eval-only; +# fixed-sequence and agentic artifacts are untouched. # lm-eval result files are ``results_.json`` (optionally a ``_concN`` / -# staging suffix). The timestamp uses dashes throughout, so it is fixed-width -# and lexicographically sortable. +# staging suffix). Timestamped names and legacy mtimes are both converted to +# epoch nanoseconds so mixed naming schemes have one coherent ordering. _TIMESTAMP_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d+)?") +_EVAL_RESULT_FORMAT = "inferencex-eval-v1" # Batched result files carry their concurrency as a ``_concN`` suffix (kept in # sync with ``collect_eval_results.CONC_SUFFIX_RE``). @@ -508,107 +647,330 @@ def _result_timestamp(name: str) -> Optional[str]: return match.group(0) if match else None +def _timestamp_ns(stamp: str) -> int: + """Convert an lm-eval filename timestamp to UTC epoch nanoseconds.""" + date, clock = stamp.split("T", 1) + hms, separator, fraction = clock.partition(".") + parsed = datetime.strptime( + f"{date}T{hms}", + "%Y-%m-%dT%H-%M-%S", + ).replace(tzinfo=timezone.utc) + epoch = datetime(1970, 1, 1, tzinfo=timezone.utc) + delta = parsed - epoch + fractional_ns = ( + int((fraction + "000000000")[:9]) if separator else 0 + ) + return ( + delta.days * 86_400_000_000_000 + + delta.seconds * 1_000_000_000 + + fractional_ns + ) + + +def _result_order(path: Path) -> tuple[int, str]: + """Return one deterministic recency key for timestamped and legacy files.""" + stamp = _result_timestamp(path.name) + try: + recency = ( + _timestamp_ns(stamp) + if stamp is not None + else path.stat().st_mtime_ns + ) + except ValueError: + recency = path.stat().st_mtime_ns + return recency, path.name + + +def _recognized_eval_result_paths(paths: Iterable[Path]) -> list[Path]: + """Return result JSONs carrying a collector-recognized eval marker.""" + recognized: list[Path] = [] + for path in paths: + try: + data = load_json(path) + except (OSError, UnicodeDecodeError, json.JSONDecodeError): + continue + if isinstance(data, dict) and ( + "lm_eval_version" in data + or data.get("result_format") == _EVAL_RESULT_FORMAT + ): + recognized.append(path) + return recognized + + +def _raw_result_error(path: Path) -> Optional[str]: + """Return a structural error for a raw result, or None when reusable.""" + try: + data = load_json(path) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + return f"is malformed JSON: {exc}" + if not isinstance(data, dict): + return "is not an object" + if "integration_error" in data: + return "reports an integration error" + if ( + "lm_eval_version" not in data + and data.get("result_format") != _EVAL_RESULT_FORMAT + ): + return "has no recognized eval result format" + + results = data.get("results") + if not isinstance(results, dict) or not results: + return "has empty or malformed results" + configs = data.get("configs", {}) + if not isinstance(configs, dict): + return "has malformed configs" + + sample_counts = data.get("n-samples") + if "n-samples" in data and not isinstance(sample_counts, dict): + return "has malformed effective sample counts" + + for task, metrics in results.items(): + if not isinstance(task, str) or not task: + return "has an invalid task name" + if not isinstance(metrics, dict) or not metrics: + return f"has empty or malformed results for task {task!r}" + task_config = configs.get(task, {}) + if not isinstance(task_config, dict): + return f"has malformed config for task {task!r}" + metric_list = task_config.get("metric_list", []) + filter_list = task_config.get("filter_list", []) + if not isinstance(metric_list, list) or not isinstance(filter_list, list): + return f"has malformed config for task {task!r}" + if metric_list: + first_metric = metric_list[0] + if ( + not isinstance(first_metric, dict) + or not isinstance(first_metric.get("metric"), str) + or not first_metric["metric"] + ): + return f"has malformed metric config for task {task!r}" + base_metric = first_metric["metric"] + else: + base_metric = "exact_match" + if filter_list: + if any( + not isinstance(item, dict) + or not isinstance(item.get("name"), str) + or not item["name"] + for item in filter_list + ): + return f"has malformed filter config for task {task!r}" + strict_names = [ + f"{base_metric},{item['name']}" + for item in filter_list + if "strict" in item["name"] or "resolved" in item["name"] + ] + primary_names = strict_names or [ + f"{base_metric},{item['name']}" + for item in filter_list + if "flex" in item["name"] or "extract" in item["name"] + ] + else: + primary_names = ["acc" if "acc" in metrics else base_metric] + if not primary_names or any(name not in metrics for name in primary_names): + return f"has no score for task {task!r}" + + for name in primary_names: + score = metrics[name] + if ( + isinstance(score, bool) + or not isinstance(score, (int, float)) + or not math.isfinite(score) + or score < 0 + or score > 1 + ): + return ( + f"has invalid score {name!r} for task {task!r}: " + f"{score!r}" + ) + if sample_counts is not None: + task_counts = sample_counts.get(task) + if not isinstance(task_counts, dict) or "effective" not in task_counts: + return f"has malformed effective sample count for task {task!r}" + effective = task_counts["effective"] + if ( + isinstance(effective, bool) + or not isinstance(effective, (int, float)) + or not math.isfinite(effective) + or effective <= 0 + ): + return ( + f"has invalid effective sample count for task {task!r}: " + f"{effective!r}" + ) + return None + + def _raw_dir_contributions( artifact_dir: Path, ) -> tuple[list[tuple[tuple[Any, ...], Optional[int]]], dict[str, Any], bool]: - """Return (identity, conc) pairs a raw dir contributes, plus its meta. - - Mirrors ``raw_eval_key_rows``: a batched artifact contributes one identity - per ``completed_eval_concs`` entry; a legacy artifact contributes one from - its meta. ``conc`` is the batched concurrency (``None`` for legacy). - """ - meta = load_json(artifact_dir / "meta_env.json") + """Return validated (identity, conc) contributions and raw metadata.""" + try: + meta = load_json(artifact_dir / "meta_env.json") + except (OSError, UnicodeDecodeError, json.JSONDecodeError): + return [], {}, False if not isinstance(meta, dict): return [], {}, False - batched = isinstance(meta.get("eval_concs"), list) - if batched: - concs = meta.get("completed_eval_concs") - if not isinstance(concs, list): - return [], meta, True - return ( - [(eval_key({**meta, "conc": conc}), as_int(conc)) for conc in concs], - meta, - True, - ) - return [(eval_key(meta), None)], meta, False + if invalid_eval_suite(meta): + return [], meta, False + contributions, batched, errors = _raw_meta_contributions( + artifact_dir.name, + meta, + ) + if errors: + return [], meta, batched + return contributions, meta, batched + + +def _source_names_raw_dir(source: Any, artifact_name: str) -> bool: + """Return whether an aggregate source path names this exact raw directory.""" + return artifact_name in re.split(r"[\\/]+", str(source or "")) def _eval_winners(artifacts_dir: Path) -> dict[tuple[Any, ...], str]: - """Pick the raw dir holding the latest result for each eval identity. + """Pick structurally valid, aggregate-backed latest raw results.""" + aggregate_sources: dict[tuple[Any, ...], list[Any]] = {} + aggregate_dir = artifacts_dir / "eval_results_all" + for path in sorted(aggregate_dir.glob("*.json")): + try: + data = load_json(path) + except (OSError, UnicodeDecodeError, json.JSONDecodeError): + continue + if not isinstance(data, list): + continue + for row in data: + if isinstance(row, dict) and not invalid_eval_suite(row): + aggregate_sources.setdefault(eval_key(row), []).append( + row.get("source") + ) - Ranks only by the lm-eval result timestamp, so an identity appears here - only when at least one of its raw dirs carries a real result. Identities - with no timestamped result get no winner and are left untouched. - """ - best: dict[tuple[Any, ...], tuple[str, str]] = {} + best: dict[ + tuple[Any, ...], + tuple[tuple[int, str], str, Path], + ] = {} for artifact_dir in raw_eval_artifact_dirs(artifacts_dir): contributions, _, batched = _raw_dir_contributions(artifact_dir) - if not contributions: - continue - for path in artifact_dir.glob("results*.json"): - stamp = _result_timestamp(path.name) - if stamp is None: + result_paths = _recognized_eval_result_paths( + artifact_dir.glob("results*.json") + ) + for key, key_conc in contributions: + candidates = [ + path + for path in result_paths + if not batched or _result_concurrency(path.name) == key_conc + ] + if not candidates: continue - conc = _result_concurrency(path.name) - for key, key_conc in contributions: - # A batched result file is tagged with its conc; only let it - # compete for the matching identity. - if batched and conc is not None and key_conc != conc: - continue - candidate = (stamp, artifact_dir.name) - if best.get(key) is None or candidate > best[key]: - best[key] = candidate - return {key: name for key, (_, name) in best.items()} + latest = max(candidates, key=_result_order) + candidate = (_result_order(latest), artifact_dir.name, latest) + current = best.get(key) + if current is None or candidate[:2] > current[:2]: + best[key] = candidate + + winners: dict[tuple[Any, ...], str] = {} + for key, (_, artifact_name, path) in best.items(): + if _raw_result_error(path) is not None: + continue + if any( + _source_names_raw_dir(source, artifact_name) + for source in aggregate_sources.get(key, []) + ): + winners[key] = artifact_name + return winners def _dedupe_eval_aggregate( artifacts_dir: Path, winners: dict[tuple[Any, ...], str] ) -> list[str]: - """Keep one aggregate row per winning identity (its winner dir's row).""" + """Keep one aggregate row per winning identity across all aggregate files.""" eval_dir = artifacts_dir / "eval_results_all" if not eval_dir.is_dir(): return [] - messages: list[str] = [] + + loaded: dict[Path, list[Any]] = {} + groups: dict[ + tuple[Any, ...], + list[tuple[Path, int, dict[str, Any]]], + ] = {} for agg_path in sorted(eval_dir.glob("*.json")): - data = load_json(agg_path) + try: + data = load_json(agg_path) + except (OSError, UnicodeDecodeError, json.JSONDecodeError): + continue if not isinstance(data, list): continue - groups: dict[tuple[Any, ...], list[int]] = {} - keep: set[int] = set() - for idx, row in enumerate(data): - if not isinstance(row, dict): - keep.add(idx) - continue - groups.setdefault(eval_key(row), []).append(idx) - for key, indices in groups.items(): - winner = winners.get(key) - # Only collapse identities with a clear latest result; leave - # ambiguous duplicates for validation to reject. - if winner is None or len(indices) == 1: - keep.update(indices) - continue - keep.add( - next( - ( - idx - for idx in indices - if winner in str(data[idx].get("source") or "") - ), - max( - indices, - key=lambda idx: _result_timestamp( - str(data[idx].get("source") or "") - ) - or "", - ), + loaded[agg_path] = data + for index, row in enumerate(data): + if isinstance(row, dict) and not invalid_eval_suite(row): + groups.setdefault(eval_key(row), []).append( + (agg_path, index, row) ) + + keep = { + path: set(range(len(data))) + for path, data in loaded.items() + } + winner_result_names: dict[tuple[Any, ...], str] = {} + for key, artifact_name in winners.items(): + artifact_dir = artifacts_dir / artifact_name + contributions, _, batched = _raw_dir_contributions(artifact_dir) + conc = next( + (candidate_conc for candidate_key, candidate_conc in contributions + if candidate_key == key), + None, + ) + candidates = [ + path + for path in _recognized_eval_result_paths( + artifact_dir.glob("results*.json") ) - if len(keep) != len(data): - kept = [row for idx, row in enumerate(data) if idx in keep] - agg_path.write_text(json.dumps(kept, indent=2)) - messages.append( - f"{agg_path.name}: kept {len(kept)} of {len(data)} eval row(s)" - ) + if not batched or _result_concurrency(path.name) == conc + ] + if candidates: + winner_result_names[key] = max(candidates, key=_result_order).name + + for key, entries in groups.items(): + winner = winners.get(key) + if winner is None or len(entries) == 1: + continue + matching = [ + entry + for entry in entries + if _source_names_raw_dir(entry[2].get("source"), winner) + ] + winner_result_name = winner_result_names.get(key) + exact_matching = [ + entry + for entry in matching + if re.split( + r"[\\/]+", + str(entry[2].get("source") or ""), + )[-1] == winner_result_name + ] + if not exact_matching: + continue + chosen = max( + exact_matching, + key=lambda entry: (entry[0].name, entry[1]), + ) + chosen_location = chosen[0], chosen[1] + for path, index, _ in entries: + if (path, index) != chosen_location: + keep[path].discard(index) + + messages: list[str] = [] + for agg_path, data in loaded.items(): + kept = [ + row + for index, row in enumerate(data) + if index in keep[agg_path] + ] + if len(kept) == len(data): + continue + agg_path.write_text(json.dumps(kept, indent=2)) + messages.append( + f"{agg_path.name}: kept {len(kept)} of {len(data)} eval row(s)" + ) return messages @@ -642,11 +1004,12 @@ def superseded(key: tuple[Any, ...]) -> bool: remaining = [ conc for conc in meta.get("completed_eval_concs", []) - if as_int(conc) not in losing + if conc not in losing ] if not remaining: shutil.rmtree(artifact_dir) return f"removed superseded batched raw eval dir {name!r}" + meta["eval_concs"] = remaining meta["completed_eval_concs"] = remaining (artifact_dir / "meta_env.json").write_text(json.dumps(meta)) dropped = ",".join(str(conc) for conc in sorted(losing)) From 209fbf4a5c9226856f3104d6f60d5c4130095e32 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:18:29 -0500 Subject: [PATCH 32/99] fix: preserve selected SRT eval concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留已选择的 SRT 评估并发度。 --- runners/patch_srt_eval_dispatch.py | 1 + runners/test_slurm_utils.py | 1 + 2 files changed, 2 insertions(+) diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index 8c1f9d5fd6..fa2697ac65 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -11,6 +11,7 @@ "IS_MULTINODE",""" DO_SWEEP_ENV_REPLACEMENT = """ "EVAL_ONLY", "EVAL_FRAMEWORK", + "EVAL_CONC", "EVAL_LIMIT", "EVAL_SUITE", "SWEBENCH_GEN_MODE", diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index eb5bf45b94..3fb037398a 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -99,6 +99,7 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert second.returncode == 0, second.stderr assert do_sweep.read_text().count('"EVAL_FRAMEWORK"') == 1 assert do_sweep.read_text().count('"EVAL_SUITE"') == 1 + assert do_sweep.read_text().count('"EVAL_CONC"') == 1 assert do_sweep.read_text().count('"EVAL_LIMIT"') == 1 assert do_sweep.read_text().count('"SWEBENCH_GEN_MODE"') == 1 assert 'run_eval --port "$PORT"' in eval_script.read_text() From 38a867eee6fc86ee3219e8548ba9c084d0e56b4c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:04:10 -0500 Subject: [PATCH 33/99] fix: preserve dsv4 thinking request mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:显式传递 DSV4 的开源思考模式,确保流式工具调用按部署配置解析。 --- benchmarks/benchmark_lib.sh | 1 + utils/evals/EVALS.md | 3 ++- utils/evals/kimi_vendor_eval.py | 19 ++++++++++++++++--- utils/evals/test_kimi_vendor_eval.py | 18 ++++++++++++++++++ utils/evals/test_run_eval_dispatch.py | 3 +++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index da1bf5df10..23de2edb4c 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1297,6 +1297,7 @@ _run_kimi_tool_call_schema_eval() { --base-url "http://127.0.0.1:${port}/v1" \ --api-key EMPTY \ --model "$model_name" \ + --model-prefix "${MODEL_PREFIX:-}" \ --output-dir "$results_dir" \ || eval_rc=$? _cleanup_kimi_vendor_eval \ diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index d1825d433d..38660e366f 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -90,7 +90,8 @@ The selected interpreter installs the minimal pinned verifier runtime upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; -- `--think-mode none --selection object --max-cases 1 --max-tokens 2048`; +- `--think-mode none` for other models, or `--think-mode opensource --thinking` + for `dsv4`, plus `--selection object --max-cases 1 --max-tokens 2048`; - the upstream-recommended `--reruns 3 --reruns-delay 2`; - the bundled Walle case directory and `--tool-json-report`. diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 8851f2be66..68d6f997d5 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -30,9 +30,19 @@ def prepare_compatibility_path(output_dir: Path) -> Path: def build_pytest_command( - *, base_url: str, api_key: str, model: str, report_path: Path + *, + base_url: str, + api_key: str, + model: str, + model_prefix: str = "", + report_path: Path, ) -> list[str]: """Build the fixed Phase 1 invocation of the upstream verifier.""" + thinking_args = ( + ["--think-mode", "opensource", "--thinking"] + if model_prefix == "dsv4" + else ["--think-mode", "none"] + ) return [ sys.executable, "-m", @@ -48,8 +58,7 @@ def build_pytest_command( api_key, "--smoke-model", model, - "--think-mode", - "none", + *thinking_args, "--selection", "object", "--max-cases", @@ -169,6 +178,7 @@ def run_evaluation( base_url: str, api_key: str, model: str, + model_prefix: str = "", output_dir: Path, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, ) -> bool: @@ -187,6 +197,7 @@ def run_evaluation( base_url=base_url, api_key=api_key, model=model, + model_prefix=model_prefix, report_path=native_report.resolve(), ), cwd=verifier_dir, @@ -236,6 +247,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--base-url") parser.add_argument("--api-key", default="EMPTY") parser.add_argument("--model", required=True) + parser.add_argument("--model-prefix", default="") parser.add_argument("--output-dir", required=True, type=Path) parser.add_argument( "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS @@ -278,6 +290,7 @@ def main(argv: Sequence[str] | None = None) -> int: base_url=args.base_url, api_key=args.api_key, model=args.model, + model_prefix=args.model_prefix, output_dir=args.output_dir, timeout_seconds=args.timeout_seconds, ) diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index a71d3d962f..9e887a7018 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -87,6 +87,24 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: ] +def test_builds_dsv4_thinking_command(tmp_path: Path) -> None: + command = kve.build_pytest_command( + base_url="http://127.0.0.1:8000/v1", + api_key="EMPTY", + model="deepseek-ai/DeepSeek-V4-Pro", + model_prefix="dsv4", + report_path=tmp_path / kve.NATIVE_REPORT_FILENAME, + ) + + think_mode_index = command.index("--think-mode") + selection_index = command.index("--selection") + assert command[think_mode_index:selection_index] == [ + "--think-mode", + "opensource", + "--thinking", + ] + + @pytest.mark.parametrize( ("stream_status", "return_code", "expected_pass", "expected_score"), ( diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 7e47084428..8dacbebda4 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -746,6 +746,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( "RESULTS_DIR": str(results_dir), "VERIFIER_DIR": str(verifier_dir), "MODEL": "test-model", + "MODEL_PREFIX": "dsv4", "RUNTIME_DIR": str(runtime_dir), "PYTHON_DIR": str(python_dir), "OPENAI_API_KEY": "must-not-be-forwarded", @@ -787,6 +788,8 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( results_dir, ): assert f"PYTHON_ARG=<{value}>" in output + assert "PYTHON_ARG=<--model-prefix>" in output + assert "PYTHON_ARG=" in output assert "must-not-be-forwarded" not in output assert "SYSTEM_PYTHON_UNEXPECTED" not in output assert "EVAL_SUITE=kimi_tool_call_schema" in output From ad9512be7b1001b02b114911634d18a23dde26b1 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:50:01 -0500 Subject: [PATCH 34/99] feat: add minimax m3 provider smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate three pinned deterministic provider-verifier cases with strict request, result, timeout, retry, and artifact contracts. Wire the smoke through single-node, multi-node, fixed-sequence, and agentic eval workflows. 中文:新增包含三个固定用例的 MiniMax M3 Provider Verifier 冒烟评估,严格约束请求、结果、超时、重试及产物格式,并接入单节点、多节点、定长序列和 Agentic 评估工作流。 --- .../workflows/benchmark-multinode-tmpl.yml | 4 +- .github/workflows/benchmark-tmpl.yml | 4 +- .github/workflows/e2e-tests.yml | 8 +- benchmarks/benchmark_lib.sh | 216 +++- utils/evals/EVALS.md | 95 +- utils/evals/minimax_m3_smoke.json | 168 +++ utils/evals/minimax_provider_eval.py | 1055 +++++++++++++++++ utils/evals/test_minimax_provider_eval.py | 756 ++++++++++++ utils/evals/test_run_eval_dispatch.py | 334 +++++- utils/evals/thresholds.yaml | 1 + utils/test_collect_eval_results.py | 21 +- 11 files changed, 2587 insertions(+), 75 deletions(-) create mode 100644 utils/evals/minimax_m3_smoke.json create mode 100755 utils/evals/minimax_provider_eval.py create mode 100644 utils/evals/test_minimax_provider_eval.py diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 7004faed99..d5e2c5c96e 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -134,12 +134,12 @@ on: required: false default: false eval-framework: - description: "Eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" type: string required: false default: "lm-eval" eval-suite: - description: "Kimi Vendor Verifier suite; leave empty for other eval runners" + description: "Provider verifier suite; leave empty for lm-eval and swebench" type: string required: false default: "" diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 27f933dec5..05055698a4 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -86,12 +86,12 @@ on: required: false default: false eval-framework: - description: "Eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" type: string required: false default: "lm-eval" eval-suite: - description: "Kimi Vendor Verifier suite; leave empty for other eval runners" + description: "Provider verifier suite; leave empty for lm-eval and swebench" type: string required: false default: "" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 978a690c5e..c779a15e73 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,12 +46,12 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Kimi Vendor Verifier suite; empty for other runners" + description: "Provider verifier suite; empty for lm-eval and swebench" required: false type: string default: "" @@ -136,12 +136,12 @@ on: type: string default: "" eval-framework: - description: "Agentic eval runner (lm-eval, swebench, or kimi-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" required: false type: string default: "lm-eval" eval-suite: - description: "Kimi Vendor Verifier suite; empty for other runners" + description: "Provider verifier suite; empty for lm-eval and swebench" required: false type: string default: "" diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 23de2edb4c..3912dcb7ec 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -892,22 +892,25 @@ _install_lm_eval_deps() { fi } -_prepare_kimi_vendor_python() { - KIMI_VENDOR_PYTHON=python3 - KIMI_VENDOR_PYTHON_CLEANUP_DIR="" - export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR +_prepare_vendor_verifier_python() { + local verifier_name="$1" + local runtime_prefix="$2" + + VENDOR_VERIFIER_PYTHON=python3 + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR if python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))'; then return 0 fi local python_dir uv_prefix uv_bin venv_dir prepare_rc=0 - python_dir="$(mktemp -d /tmp/kimi-vendor-python-XXXXXX)" || { - echo "ERROR: could not create a temporary Python directory for Kimi-Vendor-Verifier" >&2 + python_dir="$(mktemp -d "/tmp/${runtime_prefix}-XXXXXX")" || { + echo "ERROR: could not create a temporary Python directory for ${verifier_name}" >&2 return 1 } - KIMI_VENDOR_PYTHON_CLEANUP_DIR="$python_dir" - export KIMI_VENDOR_PYTHON_CLEANUP_DIR + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$python_dir" + export VENDOR_VERIFIER_PYTHON_CLEANUP_DIR uv_prefix="${python_dir}/uv" uv_bin="${uv_prefix}/bin/uv" @@ -925,24 +928,24 @@ _prepare_kimi_vendor_python() { || prepare_rc=$? fi if [ "$prepare_rc" -eq 0 ] && [ ! -x "${venv_dir}/bin/python" ]; then - echo "ERROR: pinned uv did not create the Kimi verifier Python interpreter" >&2 + echo "ERROR: pinned uv did not create the ${verifier_name} Python interpreter" >&2 prepare_rc=1 fi if [ "$prepare_rc" -ne 0 ]; then rm -rf "$python_dir" || true - KIMI_VENDOR_PYTHON=python3 - KIMI_VENDOR_PYTHON_CLEANUP_DIR="" - export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR + VENDOR_VERIFIER_PYTHON=python3 + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR return "$prepare_rc" fi - KIMI_VENDOR_PYTHON="${venv_dir}/bin/python" - export KIMI_VENDOR_PYTHON + VENDOR_VERIFIER_PYTHON="${venv_dir}/bin/python" + export VENDOR_VERIFIER_PYTHON } _install_kimi_vendor_eval_deps() { local target_dir="$1" - "${KIMI_VENDOR_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ "httpx[http2]==0.28.1" \ "openai==2.14.0" \ "jsonschema==4.25.1" \ @@ -971,7 +974,7 @@ _prepare_kimi_vendor_verifier() { return 1 } - "${KIMI_VENDOR_PYTHON:-python3}" - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? + "${VENDOR_VERIFIER_PYTHON:-python3}" - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? from pathlib import Path import re import socket @@ -1202,7 +1205,7 @@ PY printf '%s\n' "$checkout_dir" } -_cleanup_kimi_vendor_eval() { +_cleanup_vendor_eval() { local path for path in "$@"; do [ -z "$path" ] || rm -rf "$path" || true @@ -1258,7 +1261,7 @@ _run_kimi_tool_call_schema_eval() { export EVAL_RESULT_DIR="$results_dir" local setup_rc=0 integration_error="" - _prepare_kimi_vendor_python || { + _prepare_vendor_verifier_python "Kimi-Vendor-Verifier" "kimi-vendor-python" || { setup_rc=$? integration_error="Kimi Vendor Verifier Python runtime preparation failed with exit code ${setup_rc}" } @@ -1277,8 +1280,8 @@ _run_kimi_tool_call_schema_eval() { } fi if [ "$setup_rc" -ne 0 ]; then - _cleanup_kimi_vendor_eval \ - "$runtime_dir" "$checkout_dir" "${KIMI_VENDOR_PYTHON_CLEANUP_DIR:-}" + _cleanup_vendor_eval \ + "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" echo "ERROR: ${integration_error}" >&2 local artifact_rc=0 _write_kimi_vendor_integration_error \ @@ -1292,7 +1295,7 @@ _run_kimi_tool_call_schema_eval() { local eval_rc=0 PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ - "${KIMI_VENDOR_PYTHON:-python3}" "$adapter_path" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ --verifier-dir "$checkout_dir" \ --base-url "http://127.0.0.1:${port}/v1" \ --api-key EMPTY \ @@ -1300,8 +1303,8 @@ _run_kimi_tool_call_schema_eval() { --model-prefix "${MODEL_PREFIX:-}" \ --output-dir "$results_dir" \ || eval_rc=$? - _cleanup_kimi_vendor_eval \ - "$runtime_dir" "$checkout_dir" "${KIMI_VENDOR_PYTHON_CLEANUP_DIR:-}" + _cleanup_vendor_eval \ + "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$eval_rc" } @@ -1321,6 +1324,139 @@ run_kimi_vendor_eval() { esac } +_install_minimax_vendor_eval_deps() { + local target_dir="$1" + "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ + "jsonschema==4.25.1" +} + +_prepare_minimax_vendor_runtime() { + local runtime_dir install_rc=0 + runtime_dir="$(mktemp -d /tmp/minimax-vendor-runtime-XXXXXX)" || return $? + _install_minimax_vendor_eval_deps "$runtime_dir" >&2 || install_rc=$? + if [ "$install_rc" -ne 0 ]; then + rm -rf "$runtime_dir" + return "$install_rc" + fi + printf '%s\n' "$runtime_dir" +} + +_write_minimax_vendor_integration_error() { + local adapter_path="$1" + local model_name="$2" + local results_dir="$3" + local message="$4" + + # The adapter's integration-error path is stdlib-only, so it remains usable + # when Python provisioning or dependency installation is what failed. + python3 "$adapter_path" \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --integration-error "$message" +} + +_run_minimax_m3_smoke_eval() { + local port="${PORT:-8888}" + local results_dir="${EVAL_RESULT_DIR:-}" + + while [[ $# -gt 0 ]]; do + case "$1" in + --port|--results-dir) + if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then + echo "ERROR: $1 requires a value" >&2 + return 2 + fi + case "$1" in + --port) port="$2" ;; + --results-dir) results_dir="$2" ;; + esac + shift 2 + ;; + *) + echo "Unknown parameter: $1" >&2 + return 2 + ;; + esac + done + + if [ -z "$results_dir" ]; then + results_dir="$(mktemp -d /tmp/eval_out-XXXXXX)" || return $? + fi + + local model_name="${MODEL_NAME:-${MODEL:-}}" + local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/minimax_provider_eval.py" + local fixture_path="${INFERENCEX_REPO_ROOT}/utils/evals/minimax_m3_smoke.json" + local runtime_dir="" + + mkdir -p "$results_dir" || return $? + results_dir="$(cd "$results_dir" && pwd)" || return $? + export EVAL_RESULT_DIR="$results_dir" + + local setup_rc=0 integration_error="" + _prepare_vendor_verifier_python "MiniMax Provider Verifier" "minimax-vendor-python" || { + setup_rc=$? + integration_error="MiniMax Provider Verifier Python runtime preparation failed with exit code ${setup_rc}" + } + if [ "$setup_rc" -eq 0 ]; then + runtime_dir=$(_prepare_minimax_vendor_runtime) || { + setup_rc=$? + integration_error="MiniMax Provider Verifier dependency installation failed with exit code ${setup_rc}" + } + fi + if [ "$setup_rc" -ne 0 ]; then + _cleanup_vendor_eval \ + "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + echo "ERROR: ${integration_error}" >&2 + local artifact_rc=0 + _write_minimax_vendor_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write MiniMax verifier failure artifact (exit code ${artifact_rc})" >&2 + fi + return "$setup_rc" + fi + + local eval_rc=0 + PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --api-key EMPTY \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --fixture "$fixture_path" \ + --request-timeout-seconds 180 \ + --timeout-seconds 900 \ + || eval_rc=$? + _cleanup_vendor_eval \ + "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + return "$eval_rc" +} + +run_minimax_vendor_eval() { + local eval_suite="${EVAL_SUITE:-minimax_m3_smoke}" + export EVAL_SUITE="$eval_suite" + + case "$eval_suite" in + minimax_m3_smoke) + local model_name="${MODEL_NAME:-${MODEL:-}}" + local model_prefix="${MODEL_PREFIX:-}" + if [[ "$model_prefix" != [Mm][Ii][Nn][Ii][Mm][Aa][Xx][Mm]3 ]] \ + && [[ "$model_name" != *[Mm][Ii][Nn][Ii][Mm][Aa][Xx]-[Mm]3* ]]; then + echo "ERROR: MiniMax M3 smoke requires MODEL_PREFIX=minimaxm3 or a MODEL/MODEL_NAME containing MiniMax-M3" >&2 + export EVAL_RESULT_DIR="" + return 2 + fi + _run_minimax_m3_smoke_eval "$@" + ;; + *) + echo "ERROR: unsupported MiniMax Provider Verifier suite '${eval_suite}'" >&2 + export EVAL_RESULT_DIR="" + return 2 + ;; + esac +} + _eval_patches_dir() { printf '%s\n' "${INFERENCEX_REPO_ROOT}/utils/evals/patches" } @@ -2136,9 +2272,14 @@ run_eval() { fi local framework="${EVAL_FRAMEWORK:-${cli_framework:-$scenario_default}}" - if [ "$framework" = "kimi-vendor" ] && [ -z "${EVAL_SUITE:-}" ]; then - EVAL_SUITE="kimi_tool_call_schema" - fi + case "$framework" in + kimi-vendor) + [ -n "${EVAL_SUITE:-}" ] || EVAL_SUITE="kimi_tool_call_schema" + ;; + minimax-vendor) + [ -n "${EVAL_SUITE:-}" ] || EVAL_SUITE="minimax_m3_smoke" + ;; + esac case "${EVAL_SUITE:-}" in "") ;; @@ -2148,14 +2289,18 @@ run_eval() { ;; esac - if [ -n "${EVAL_SUITE:-}" ] && [ "$framework" != "kimi-vendor" ]; then - echo "ERROR: EVAL_SUITE is only supported with EVAL_FRAMEWORK=kimi-vendor" >&2 + if [ -n "${EVAL_SUITE:-}" ] \ + && [ "$framework" != "kimi-vendor" ] \ + && [ "$framework" != "minimax-vendor" ]; then + echo "ERROR: EVAL_SUITE is only supported with a provider verifier framework (kimi-vendor or minimax-vendor)" >&2 return 2 fi - # Kimi Vendor Verifier uses a fixed request budget and does not consume - # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for that path. - if [ "$framework" != "kimi-vendor" ] && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then + # Provider verifier suites use fixed request budgets and do not consume + # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for those paths. + if [ "$framework" != "kimi-vendor" ] \ + && [ "$framework" != "minimax-vendor" ] \ + && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then compute_eval_context_length "$MODEL" "${MAX_MODEL_LEN:-0}" > /dev/null fi @@ -2227,6 +2372,7 @@ run_eval() { lm-eval|lm_eval) run_lm_eval "${forwarded[@]}" || eval_rc=$? ;; swebench) run_swebench_eval "${forwarded[@]}" || eval_rc=$? ;; kimi-vendor) run_kimi_vendor_eval "${forwarded[@]}" || eval_rc=$? ;; + minimax-vendor) run_minimax_vendor_eval "${forwarded[@]}" || eval_rc=$? ;; *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac @@ -2234,10 +2380,12 @@ run_eval() { export EVAL_COMPLETED_SUITE="$EVAL_SUITE" fi - # Agentic eval-only recipes have no separate staging step. Kimi failures - # also carry diagnostic score artifacts that callers must preserve. + # Agentic eval-only recipes have no separate staging step. Provider + # verifier failures also carry diagnostic score artifacts to preserve. if { [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; } \ - || { [ "$framework" = "kimi-vendor" ] && [ "$eval_rc" -ne 0 ]; }; then + || { { [ "$framework" = "kimi-vendor" ] \ + || [ "$framework" = "minimax-vendor" ]; } \ + && [ "$eval_rc" -ne 0 ]; }; then append_lm_eval_summary || true fi diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 38660e366f..9daae9800d 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -65,9 +65,9 @@ python3 utils/evals/validate_scores.py The framework selects a provider-specific subprocess adapter, while the suite selects a case set understood by that adapter. Each adapter owns its endpoint -format, dependencies, native report, metrics, and pass policy. Future MiniMax -or BFCL support should add explicit `run_eval` cases rather than a shared -request or report abstraction. +format, dependencies, native report, metrics, and pass policy. Kimi and MiniMax +use separate explicit `run_eval` cases; future provider or BFCL support should +do the same rather than introduce a shared request or report abstraction. Agentic eval jobs forward the matrix `spec-decoding` value, so MTP entries launch their existing `*_mtp.sh` server instead of silently falling back to STP. @@ -118,6 +118,87 @@ two-case smoke against their OpenAI-compatible frontend. Eval-only launchers restore real block verification before submitting recipes that otherwise use synthetic acceptance for throughput. +### MiniMax M3 provider compatibility smoke + +The Phase 1 MiniMax smoke is opt-in and applies only to a MiniMax M3 provider +exposing an OpenAI-compatible chat-completions API. The runner rejects other +models unless `MODEL_PREFIX=minimaxm3` or `MODEL`/`MODEL_NAME` contains +`MiniMax-M3`. Select +`eval-framework: minimax-vendor` and `eval-suite: minimax_m3_smoke` in +`e2e-tests.yml`, or run it from the repository root against an already-ready +server: + +```bash +source benchmarks/benchmark_lib.sh +export EVAL_FRAMEWORK=minimax-vendor +export MODEL_NAME="" +export EVAL_SUITE=minimax_m3_smoke +export EVAL_RESULT_DIR="$(mktemp -d /tmp/eval_out-XXXXXX)" +run_eval --port "$PORT" +append_lm_eval_summary +python3 utils/evals/validate_scores.py +``` + +`utils/evals/minimax_m3_smoke.json` is derived from +[MiniMax-AI/MiniMax-Provider-Verifier](https://github.com/MiniMax-AI/MiniMax-Provider-Verifier) +`sample.jsonl` at commit +`85bf180e54e2ab0b31595cfdc697116c4760876d`. The vendored fixture retains +the full upstream MIT copyright, permission, and warranty notice. It contains +exactly these three upstream zero-based rows, in this order: + +1. index 0, the non-Cyrillic language-following check; +2. index 71, an `expected_tool_call: true` request exercising tool-call trigger + and argument-schema validation; +3. index 101, an `expected_tool_call: false` request exercising the scenario + parameter key-order check. + +The adapter applies the pinned validator semantics directly to this fixture; it +does not download the upstream repository or run the remaining 99 cases. + +The adapter sends the three requests sequentially to +`${base_url}/chat/completions`. The endpoint must accept an OpenAI-compatible +Bearer token and chat-completions request body and return OpenAI-compatible +message, finish-reason, and tool-call fields. Redirect responses are rejected +before forwarding bearer credentials; the local runner supplies +`Authorization: Bearer EMPTY`. The smoke overrides fixture +sampling with `temperature: 0`, `top_p: 1`, and `max_tokens: 2048`. Each request +has a 180-second timeout by default and at most one retry for transport +failures, HTTP 429, or HTTP 5xx responses (two total attempts); a hard +900-second global bound covers the whole suite. + +`minimax_vendor_report.json` is the native report. It preserves every raw +response and reports the six requested upstream-derived metric families: + +- `Query-Success-Rate`, which records whether the endpoint returned a response, + not whether the answer was generally correct; +- `ToolCalls-Trigger-Similarity`, the F1 score computed from the fixture's + `expected_tool_call` labels; +- `ToolCalls-Schema-Accuracy`, which validates returned function names and + arguments against the requested tool schema; +- `Error-Only-Reasoning-Rate`, for responses with reasoning but neither visible + content nor tool calls; +- `Language-Following-Success-Rate`, the non-Cyrillic language check; +- `Scenario-Check-Pass-Rate`, the scenario parameter key-order check. + +The adapter additionally writes exactly one timestamped +`results_minimax_vendor_*.json` compatibility artifact. Its `result_format` is +`inferencex-eval-v1`, `eval_adapter` is `minimax-provider-verifier`, task is +`minimax_m3_smoke`, and primary metric is `exact_match,strict-match`. A +completed run records original and effective sample counts of three. Its +compatibility score is passed cases divided by +three; the `minimax_m3_smoke` threshold is `1.0`, so every case must pass. +Both artifacts match the workflows' existing `results*.json` and +`*_vendor_report.json` upload patterns. +Setup, integration, timeout, and collection failures still emit a zero-score +compatibility artifact with error metadata. + +This is a fixed three-case provider compatibility smoke, not the full +102-case MiniMax Provider Verifier, BFCL, or a cross-model quality comparison. +It does not estimate the upstream dataset's aggregate rates, stochastic +pass-at-k behavior, streaming behavior, parallel-call behavior, multi-turn tool +execution, or general agent quality. Its metric denominators are only the +applicable cases in this pinned three-row fixture. + ### Benchmark script flow All benchmark scripts in `benchmarks/` follow one of two flows: @@ -148,10 +229,12 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_eval` | Unified entrypoint - dispatches to framework-specific runner | | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | +| `run_minimax_vendor_eval` | Validates MiniMax M3 applicability and runs the pinned three-case provider smoke | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | -| `_prepare_kimi_vendor_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime on older images | +| `_prepare_vendor_verifier_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime for provider verifiers | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | +| `_prepare_minimax_vendor_runtime` | Installs the pinned MiniMax adapter dependency in an isolated temp path | | `_prepare_kimi_vendor_verifier` | Downloads and safely extracts a fresh subset of the pinned source archive | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | @@ -241,8 +324,8 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' |----------|---------|-------------| | `RUN_EVAL` | `false` | Enable eval after throughput benchmark | | `EVAL_ONLY` | `false` | Skip throughput, only run evals (set by workflow) | -| `EVAL_FRAMEWORK` | `lm-eval` | Eval runner (`lm-eval`, `swebench`, or `kimi-vendor`) | -| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Provider suite selector and artifact identity. External override is currently supported only by `kimi-vendor`; other runners derive it from their task | +| `EVAL_FRAMEWORK` | `lm-eval` | Eval runner (`lm-eval`, `swebench`, `kimi-vendor`, or `minimax-vendor`) | +| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Provider suite selector and artifact identity. External overrides are supported by `kimi-vendor` and `minimax-vendor`; other runners derive it from their task | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | diff --git a/utils/evals/minimax_m3_smoke.json b/utils/evals/minimax_m3_smoke.json new file mode 100644 index 0000000000..9826a361da --- /dev/null +++ b/utils/evals/minimax_m3_smoke.json @@ -0,0 +1,168 @@ +{ + "source": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/85bf180e54e2ab0b31595cfdc697116c4760876d/sample.jsonl", + "ref": "85bf180e54e2ab0b31595cfdc697116c4760876d", + "indices": [ + 0, + 71, + 101 + ], + "license": "MIT License\n\nCopyright (c) 2025 MiniMax Provider Verifier Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", + "rows": [ + { + "data_index": 0, + "messages": [ + { + "role": "user", + "content": "日本ではどのような時にお年玉を渡しますか?" + } + ], + "check_type": [ + "contains_russian_characters_unicode" + ] + }, + { + "data_index": 71, + "messages": [ + { + "role": "system", + "content": "You are MiniMax M2, an AI assistant developed by Minimax. You are a helpful assistant." + }, + { + "role": "user", + "content": "Run a duplicate therapy check for patient P-009417 using the medication list at /mnt/clinical/emr/patients/P-009417/med_list_v3.json. Flag duplicates at the RxNorm ingredient level, consider only active meds (ignore discontinued and on_hold), include OTC items, require the same route to count as a duplicate, and return a detailed JSON report." + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "flag_duplicate_therapies", + "description": "Analyze a patient's medication list to identify potential duplicate therapies by ingredient or class. Supports RxNorm and ATC classification, status filtering, route matching, and OTC inclusion. Returns structured results.", + "parameters": { + "type": "object", + "properties": { + "patient_id": { + "type": "string", + "description": "Patient identifier" + }, + "med_list_path": { + "type": "string", + "description": "Absolute file path to the medication list (JSON or CSV)" + }, + "classification_scheme": { + "type": "string", + "description": "Classification used for duplicate detection", + "enum": [ + "rxnorm_ingredient", + "rxnorm_clinical_drug", + "atc_class" + ] + }, + "overlap_policy": { + "type": "string", + "description": "How to determine concurrent therapy", + "enum": [ + "current_only", + "date_overlap" + ] + }, + "concurrency_window_days": { + "type": "integer", + "description": "If overlap_policy is date_overlap, the window in days to consider therapies overlapping (0 means exact date overlap)", + "minimum": 0 + }, + "strict_route_matching": { + "type": "boolean", + "description": "Flag as duplicate only when routes match (e.g., oral vs inhalation)" + }, + "include_otc": { + "type": "boolean", + "description": "Include over-the-counter medications in the analysis" + }, + "ignore_statuses": { + "type": "array", + "description": "Medication statuses to exclude from analysis", + "items": { + "type": "string", + "enum": [ + "active", + "on_hold", + "discontinued" + ] + } + }, + "output_format": { + "type": "string", + "description": "Output report format", + "enum": [ + "summary", + "detailed_json" + ] + } + }, + "required": [ + "patient_id", + "med_list_path" + ] + } + } + } + ], + "expected_tool_call": true + }, + { + "data_index": 101, + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "你好, 按看到的原始顺序复述一下 example 工具的参数, 注意就用看到的原始顺序、不要自行排序." + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "example", + "parameters": { + "type": "object", + "properties": { + "123": { + "type": "string", + "description": "..." + }, + "some-parameter": { + "type": "string", + "description": "..." + }, + "xyz": { + "type": "string", + "description": "..." + }, + "another-parameter": { + "type": "string", + "description": "..." + } + }, + "required": [ + "some-parameter", + "xyz", + "123", + "another-parameter" + ] + } + } + } + ], + "temperature": 1, + "top_p": 0.95, + "check_type": [ + "scenario_check" + ], + "expected_tool_call": false + } + ] +} diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py new file mode 100755 index 0000000000..a8f6a82394 --- /dev/null +++ b/utils/evals/minimax_provider_eval.py @@ -0,0 +1,1055 @@ +#!/usr/bin/env python3 +"""Run the pinned three-case MiniMax M3 provider compatibility smoke.""" + +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import math +import re +import time +import urllib.error +import urllib.parse +import urllib.request +from collections.abc import Callable, Mapping, Sequence +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +TASK_NAME = "minimax_m3_smoke" +NATIVE_REPORT_FILENAME = "minimax_vendor_report.json" +COMPATIBILITY_GLOB = "results_minimax_vendor_*.json" +DEFAULT_FIXTURE_PATH = Path(__file__).with_name("minimax_m3_smoke.json") +DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 +DEFAULT_TIMEOUT_SECONDS = 900.0 +RESULT_FORMAT = "inferencex-eval-v1" +ADAPTER_NAME = "minimax-provider-verifier" +EXPECTED_INDICES = (0, 71, 101) +UPSTREAM_REF = "85bf180e54e2ab0b31595cfdc697116c4760876d" +UPSTREAM_SOURCE = ( + "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/" + f"{UPSTREAM_REF}/sample.jsonl" +) +EXPECTED_LICENSE_SHA256 = ( + "aa7cec386fcb5e555aba0e8b1c31307940af41967708c9bc0f78b4e02e235dd5" +) +EXPECTED_CASE_SHA256 = { + 0: "655d3135fc553b08c376f363165699548396428136fd9536345cf37b564b357a", + 71: "10272004ae08f4a7d08d2306404f6cbb7bbfa794230e1082a235ded036d550ed", + 101: "10c3c2bf8d4e43d520de8ef3955cda1dcdd852c9c904decbc7d8cd040431afd4", +} +MAX_RESPONSE_BYTES = 16 * 1024 * 1024 +MAX_ATTEMPTS = 2 + +HttpPost = Callable[..., Any] +Clock = Callable[[], float] + + +class TransportError(OSError): + """An HTTP transport failure that may be retried once.""" + + +class SuiteTimeoutError(TimeoutError): + """The global MiniMax smoke deadline was exhausted.""" + + +class _RejectRedirects(urllib.request.HTTPRedirectHandler): + """Keep bearer credentials on the configured endpoint.""" + + def redirect_request(self, *args: Any, **kwargs: Any) -> None: + return None + + +_NO_REDIRECT_OPENER = urllib.request.build_opener(_RejectRedirects()) + + +def _mapping(value: Any, name: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + raise TypeError(f"{name} must be an object") + return value + + +def _positive_number(value: Any, name: str) -> float: + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + or value <= 0 + ): + raise ValueError(f"{name} must be a positive finite number") + return float(value) + + +def _positive_float(value: str) -> float: + try: + return _positive_number(float(value), "value") + except (TypeError, ValueError) as exc: + raise argparse.ArgumentTypeError("must be a positive finite number") from exc + + +def _validate_messages(value: Any, name: str) -> None: + if not isinstance(value, list) or not value: + raise ValueError(f"{name} must be a non-empty array") + for index, message in enumerate(value): + item = _mapping(message, f"{name}[{index}]") + if not isinstance(item.get("role"), str) or not isinstance( + item.get("content"), str + ): + raise TypeError(f"{name}[{index}] must contain string role and content") + + +def _validate_tools(value: Any, name: str) -> None: + if not isinstance(value, list) or not value: + raise ValueError(f"{name} must be a non-empty array") + for index, tool in enumerate(value): + item = _mapping(tool, f"{name}[{index}]") + function = _mapping(item.get("function"), f"{name}[{index}].function") + if item.get("type") != "function" or not isinstance(function.get("name"), str): + raise ValueError(f"{name}[{index}] must define a named function") + parameters = _mapping( + function.get("parameters"), f"{name}[{index}].function.parameters" + ) + _mapping( + parameters.get("properties"), + f"{name}[{index}].function.parameters.properties", + ) + + +def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: + """Load and validate the exact pinned three-case fixture.""" + root = _mapping(json.loads(path.read_text(encoding="utf-8")), "fixture") + if root.get("source") != UPSTREAM_SOURCE or root.get("ref") != UPSTREAM_REF: + raise ValueError("fixture source or ref does not match the pinned upstream") + if root.get("indices") != list(EXPECTED_INDICES): + raise ValueError("fixture indices must be exactly [0, 71, 101]") + license_text = root.get("license") + if ( + not isinstance(license_text, str) + or hashlib.sha256(license_text.encode()).hexdigest() != EXPECTED_LICENSE_SHA256 + ): + raise ValueError("fixture must preserve the complete upstream MIT notice") + + raw_rows = root.get("rows") + if not isinstance(raw_rows, list) or len(raw_rows) != len(EXPECTED_INDICES): + raise ValueError("fixture must contain exactly three rows") + + rows: list[dict[str, Any]] = [] + expected_checks = { + 0: ["contains_russian_characters_unicode"], + 71: [], + 101: ["scenario_check"], + } + for position, raw_row in enumerate(raw_rows): + row = dict(_mapping(raw_row, f"fixture.rows[{position}]")) + data_index = row.get("data_index") + if data_index != EXPECTED_INDICES[position]: + raise ValueError("fixture rows must retain upstream order and data_index") + case_digest = hashlib.sha256( + json.dumps( + row, + ensure_ascii=False, + separators=(",", ":"), + ).encode() + ).hexdigest() + if case_digest != EXPECTED_CASE_SHA256[data_index]: + raise ValueError(f"fixture row {data_index} differs from pinned upstream") + _validate_messages(row.get("messages"), f"fixture.rows[{position}].messages") + check_types = row.get("check_type", []) + if check_types != expected_checks[data_index]: + raise ValueError(f"fixture row {data_index} has unexpected check_type") + if data_index == 0: + if "expected_tool_call" in row or "tools" in row: + raise ValueError("fixture row 0 must remain the language-only case") + else: + _validate_tools(row.get("tools"), f"fixture.rows[{position}].tools") + expected_label = data_index == 71 + if row.get("expected_tool_call") is not expected_label: + raise ValueError( + f"fixture row {data_index} has an invalid expected label" + ) + rows.append(copy.deepcopy(row)) + + return dict(root), rows + + +def build_endpoint(base_url: str) -> str: + if not isinstance(base_url, str) or not base_url.strip(): + raise ValueError("base_url must be a non-empty string") + normalized = base_url.strip().rstrip("/") + parsed = urllib.parse.urlsplit(normalized) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise ValueError("base_url must be an absolute HTTP(S) URL") + return f"{normalized}/chat/completions" + + +def prepare_request(row: Mapping[str, Any], model: str) -> dict[str, Any]: + """Strip evaluator fields and apply the fixed smoke sampling overrides.""" + if not isinstance(model, str) or not model.strip(): + raise ValueError("model must be a non-empty string") + request = copy.deepcopy(dict(row)) + for field in ("data_index", "check_type", "expected_tool_call", "scenario_check"): + request.pop(field, None) + request.update( + model=model, + temperature=0, + top_p=1, + max_tokens=2048, + ) + return request + + +def _read_response_body(response: Any, deadline: float) -> bytes: + chunks: list[bytes] = [] + total = 0 + read_chunk = getattr(response, "read1", response.read) + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("chat completion response exceeded its deadline") + sock = getattr( + getattr(getattr(response, "fp", None), "raw", None), + "_sock", + None, + ) + if sock is not None: + sock.settimeout(remaining) + chunk = read_chunk(64 * 1024) + if not chunk: + return b"".join(chunks) + chunks.append(chunk) + total += len(chunk) + if total > MAX_RESPONSE_BYTES: + raise ValueError( + f"chat completion response exceeds {MAX_RESPONSE_BYTES} bytes" + ) + + +def _default_http_post( + *, + url: str, + headers: Mapping[str, str], + payload: Mapping[str, Any], + timeout_seconds: float, +) -> Any: + deadline = time.monotonic() + timeout_seconds + request = urllib.request.Request( + url, + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers=dict(headers), + method="POST", + ) + try: + with _NO_REDIRECT_OPENER.open(request, timeout=timeout_seconds) as response: + content = _read_response_body(response, deadline).decode("utf-8") + except urllib.error.HTTPError as exc: + if exc.code == 429 or 500 <= exc.code < 600: + raise TransportError(f"HTTP {exc.code}: {exc.reason}") from exc + raise ValueError( + f"chat completion request failed with HTTP {exc.code}: {exc.reason}" + ) from exc + except (urllib.error.URLError, TimeoutError, OSError) as exc: + raise TransportError(str(exc)) from exc + try: + return json.loads(content) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError(f"chat completion response is not valid JSON: {exc}") from exc + + +def _validate_chat_completion_response(value: Any) -> Mapping[str, Any]: + response = _mapping(value, "chat completion response") + choices = response.get("choices") + if not isinstance(choices, list) or not choices: + raise ValueError( + "chat completion response must contain a non-empty choices array" + ) + choice = _mapping(choices[0], "chat completion response.choices[0]") + if not isinstance(choice.get("finish_reason"), str): + raise TypeError("chat completion response must contain a finish_reason") + message = _mapping( + choice.get("message"), + "chat completion response.choices[0].message", + ) + content = message.get("content") + if content is not None and not isinstance(content, str): + raise TypeError("chat completion message content must be a string or null") + tool_calls = message.get("tool_calls") + if tool_calls is not None and not isinstance(tool_calls, list): + raise TypeError("chat completion message tool_calls must be an array") + if choice["finish_reason"] == "tool_calls": + if not tool_calls: + raise ValueError( + "tool_calls finish reason requires at least one message tool call" + ) + elif tool_calls: + raise ValueError("message tool calls require a tool_calls finish reason") + return response + + +# Adapted verbatim from pinned validator/tool_calls.py. +_COMMON_COMMANDS = [ + "ls ", + "cat ", + "git ", + "npm ", + "npx ", + "cd ", + "cp ", + "mv ", + "rm ", + "mkdir ", + "chmod ", + "chown ", + "find ", + "grep ", + "curl ", + "wget ", + "pip ", +] + + +def _is_shell_c_invocation(cmd: list[Any]) -> bool: + if not cmd or len(cmd) < 3: + return False + shell = cmd[0] + if shell not in ( + "bash", + "sh", + "zsh", + "/bin/bash", + "/bin/sh", + "/bin/zsh", + "/usr/bin/bash", + "/usr/bin/sh", + "/usr/bin/zsh", + ): + return False + for arg in cmd[1:]: + if arg in ("-c", "-lc"): + return True + if arg in ("-l", "--login"): + continue + break + return False + + +def is_valid_array_command(cmd: Any) -> bool: + if not isinstance(cmd, list) or len(cmd) == 0: + return False + if _is_shell_c_invocation(cmd): + return True + for elem in cmd: + if not isinstance(elem, str): + return False + if " " in elem: + for prefix in _COMMON_COMMANDS: + if elem.startswith(prefix): + return False + return not (len(cmd) == 1 and " " in cmd[0]) + + +def validate_tool_call(tool_call: Any, tools: list[dict[str, Any]]) -> bool: + """Apply pinned JSON Schema and array-command validation lazily.""" + try: + # Lazy by design: --integration-error must work if dependency setup failed. + from jsonschema import ValidationError, validate + except ImportError: + return False + + try: + call = _mapping(tool_call, "tool_call") + function = _mapping(call["function"], "tool_call.function") + tool_name = function["name"] + schema = next( + ( + tool["function"]["parameters"] + for tool in tools + if tool["function"]["name"] == tool_name + ), + None, + ) + if not schema: + return False + args = function["arguments"] + if isinstance(args, str): + args = json.loads(args) + validate(instance=args, schema=schema) + for param_name, param_schema in schema.get("properties", {}).items(): + if ( + param_name == "command" + and param_schema.get("type") == "array" + and param_schema.get("items", {}).get("type") == "string" + ): + cmd_value = args.get(param_name) + if cmd_value is not None and not is_valid_array_command(cmd_value): + return False + return True + except (json.JSONDecodeError, ValidationError): + return False + except Exception: # noqa: BLE001 - upstream data can fail in arbitrary shapes + return False + + +def validate_tool_calls( + request: dict[str, Any], response: Any, status: str +) -> dict[str, Any]: + result: dict[str, Any] = { + "tool_calls_finish_reason": None, + "tool_calls_valid": None, + "tool_calls_count": 0, + } + if status != "success" or not response or "choices" not in response: + return result + choice = response["choices"][0] if response["choices"] else {} + finish_reason = choice.get("finish_reason") + result["tool_calls_finish_reason"] = finish_reason + if finish_reason == "tool_calls": + tools = request.get("tools", []) + tool_calls = choice.get("message", {}).get("tool_calls", []) + result["tool_calls_count"] = len(tool_calls) + if tool_calls: + result["tool_calls_valid"] = all( + validate_tool_call(tool_call, tools) for tool_call in tool_calls + ) + else: + result["tool_calls_valid"] = False + return result + + +# Adapted verbatim from pinned validator/russian_characters.py. +def not_contains_russian_characters_unicode(text: str) -> bool: + for char in text: + char_code = ord(char) + if 0x0400 <= char_code <= 0x04FF: + return False + return True + + +def validate_language(status: str, resp_content: Any) -> dict[str, Any]: + result: dict[str, Any] = { + "language_following_checked": False, + "language_following_valid": None, + } + if status != "success" or not resp_content: + return result + result["language_following_checked"] = True + result["language_following_valid"] = not_contains_russian_characters_unicode( + resp_content + ) + return result + + +# Adapted verbatim from pinned validator/scenario_check.py. +def _extract_expected_order(request: dict[str, Any]) -> list[str] | None: + tools = request.get("tools") + if not tools or not isinstance(tools, list): + return None + params = tools[0].get("function", {}).get("parameters", {}) + if not params: + return None + if "properties" in params: + return list(params["properties"].keys()) + schema_keywords = { + "type", + "description", + "required", + "additionalProperties", + "$schema", + "items", + "enum", + "default", + } + keys = [key for key in params if key not in schema_keywords] + return keys if keys else None + + +def _get_visible_content(text: str) -> str: + return re.sub(r".*?", "", text, flags=re.DOTALL).strip() + + +def _extract_actual_order(text: str, expected: list[str]) -> list[str]: + positions = [] + for param in expected: + index = text.find(param) + if index != -1: + positions.append((index, param)) + positions.sort(key=lambda item: item[0]) + return [param for _, param in positions] + + +def validate_scenario( + request: dict[str, Any], status: str, resp_content: Any +) -> dict[str, Any]: + result: dict[str, Any] = { + "scenario_check_checked": False, + "scenario_check_valid": None, + "scenario_check_detail": None, + } + if status != "success" or not resp_content: + return result + expected_order = _extract_expected_order(request) + if not expected_order: + return result + visible = _get_visible_content(resp_content) + actual_order = _extract_actual_order(visible, expected_order) + result["scenario_check_checked"] = True + result["scenario_check_valid"] = ( + len(actual_order) >= 2 and actual_order == expected_order[: len(actual_order)] + ) + result["scenario_check_detail"] = { + "expected": expected_order, + "actual": actual_order, + } + return result + + +# Adapted verbatim from pinned verify.py::_is_error_only_reasoning_response. +def _is_error_only_reasoning_response(response: Any) -> bool: + try: + if not response or "choices" not in response or not response["choices"]: + return False + message = response["choices"][0].get("message") or {} + reasoning = message.get("reasoning") or "" + content = message.get("content") or "" + tool_calls = message.get("tool_calls") + if isinstance(tool_calls, list): + has_tool_calls = len(tool_calls) > 0 + else: + has_tool_calls = bool(tool_calls) + return bool(reasoning) and (not content) and (not has_tool_calls) + except Exception: # noqa: BLE001 - mirrors the pinned upstream guard + return False + + +def _choice_fields(response: Any) -> tuple[Any, Any]: + if not isinstance(response, Mapping): + return None, None + choices = response.get("choices") + if not isinstance(choices, list) or not choices: + return None, None + choice = choices[0] + if not isinstance(choice, Mapping): + return None, None + message = choice.get("message") + content = message.get("content") if isinstance(message, Mapping) else None + return choice.get("finish_reason"), content + + +def _error_dict(exc: BaseException) -> dict[str, str]: + return {"type": type(exc).__name__, "message": str(exc)} + + +def _evaluate_case( + *, + row: dict[str, Any], + model: str, + endpoint: str, + api_key: str, + request_timeout_seconds: float, + deadline: float, + http_post: HttpPost, + clock: Clock, +) -> dict[str, Any]: + prepared = prepare_request(row, model) + started = clock() + response: Any = None + status = "failed" + attempts = 0 + request_error: BaseException | None = None + suite_timed_out = False + + for attempt in range(MAX_ATTEMPTS): + remaining = deadline - clock() + if remaining <= 0: + request_error = SuiteTimeoutError("global suite timeout exceeded") + suite_timed_out = True + break + attempts += 1 + try: + raw_response = http_post( + url=endpoint, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + payload=prepared, + timeout_seconds=min(request_timeout_seconds, remaining), + ) + if deadline - clock() <= 0: + request_error = SuiteTimeoutError("global suite timeout exceeded") + response = None + suite_timed_out = True + break + response = copy.deepcopy( + dict(_validate_chat_completion_response(raw_response)) + ) + status = "success" + request_error = None + break + except (TransportError, TimeoutError, OSError) as exc: + if deadline - clock() <= 0: + request_error = SuiteTimeoutError("global suite timeout exceeded") + suite_timed_out = True + break + request_error = exc + if attempt + 1 < MAX_ATTEMPTS: + continue + break + except Exception as exc: # noqa: BLE001 - preserve per-request diagnostics + request_error = exc + if deadline - clock() <= 0: + request_error = SuiteTimeoutError("global suite timeout exceeded") + suite_timed_out = True + break + + finish_reason, resp_content = _choice_fields(response) + result: dict[str, Any] = { + "data_index": row["data_index"], + "status": status, + "attempts": attempts, + "duration_ms": round(max(0.0, clock() - started) * 1000, 3), + "expected_tool_call": row.get("expected_tool_call"), + "finish_reason": finish_reason, + "response": response + if response is not None + else {"error": _error_dict(request_error or RuntimeError("request failed"))}, + "error_only_reasoning_checked": 1, + "error_only_reasoning": _is_error_only_reasoning_response(response), + } + + check_types = row.get("check_type", []) + try: + if check_types: + if "contains_russian_characters_unicode" in check_types: + result.update(validate_language(status, resp_content)) + if "scenario_check" in check_types: + result.update(validate_scenario(prepared, status, resp_content)) + else: + result.update(validate_tool_calls(prepared, response, status)) + except Exception as exc: # noqa: BLE001 - validators must not abort the report + result["validator_error"] = _error_dict(exc) + + failures: list[str] = [] + if status != "success": + failures.append("query_failed") + if result["error_only_reasoning"]: + failures.append("error_only_reasoning") + expected_tool_call = row.get("expected_tool_call") + if isinstance(expected_tool_call, bool): + expected_finish_reason = "tool_calls" if expected_tool_call else "stop" + actual_tool_call = finish_reason == "tool_calls" + if finish_reason != expected_finish_reason: + failures.append("tool_call_trigger") + if ( + expected_tool_call + and actual_tool_call + and result.get("tool_calls_valid") is not True + ): + failures.append("tool_call_schema") + if ( + "contains_russian_characters_unicode" in check_types + and result.get("language_following_valid") is not True + ): + failures.append("language_following") + if ( + "scenario_check" in check_types + and result.get("scenario_check_valid") is not True + ): + failures.append("scenario_check") + if "validator_error" in result: + failures.append("validator_error") + + result["case_passed"] = not failures + result["failures"] = failures + result["suite_timed_out"] = suite_timed_out + return result + + +def _ratio(numerator: int, denominator: int) -> float: + return numerator / denominator if denominator else 0.0 + + +def _summarize( + results: list[dict[str, Any]], +) -> tuple[dict[str, Any], dict[str, float]]: + total = len(results) + success_count = sum(result.get("status") == "success" for result in results) + passed_count = sum(result.get("case_passed") is True for result in results) + + labeled = [ + result + for result in results + if result.get("expected_tool_call") is True + or result.get("expected_tool_call") is False + ] + true_positive = sum( + result["expected_tool_call"] is True + and result.get("finish_reason") == "tool_calls" + for result in labeled + ) + false_negative = sum( + result["expected_tool_call"] is True + and result.get("finish_reason") != "tool_calls" + for result in labeled + ) + false_positive = sum( + result["expected_tool_call"] is False + and result.get("finish_reason") == "tool_calls" + for result in labeled + ) + expected_tool_finish_stop = sum( + result["expected_tool_call"] is True and result.get("finish_reason") == "stop" + for result in labeled + ) + expected_stop_finish_stop = sum( + result["expected_tool_call"] is False and result.get("finish_reason") == "stop" + for result in labeled + ) + precision = _ratio(true_positive, true_positive + false_positive) + recall = _ratio(true_positive, true_positive + false_negative) + trigger_f1 = ( + 2 * precision * recall / (precision + recall) if precision + recall else 0.0 + ) + schema_successes = sum( + result.get("expected_tool_call") is True + and result.get("finish_reason") == "tool_calls" + and result.get("tool_calls_valid") is True + for result in labeled + ) + + language_checked = sum( + result.get("language_following_checked") is True for result in results + ) + language_valid = sum( + result.get("language_following_valid") is True for result in results + ) + scenario_checked = sum( + result.get("scenario_check_checked") is True for result in results + ) + scenario_valid = sum( + result.get("scenario_check_valid") is True for result in results + ) + reasoning_errors = sum( + result.get("error_only_reasoning") is True for result in results + ) + + metrics = { + "Query-Success-Rate": _ratio(success_count, total), + "ToolCalls-Trigger-Similarity": trigger_f1, + "ToolCalls-Schema-Accuracy": _ratio(schema_successes, true_positive), + "Error-Only-Reasoning-Rate": _ratio(reasoning_errors, total), + "Language-Following-Success-Rate": _ratio(language_valid, language_checked), + "Scenario-Check-Pass-Rate": _ratio(scenario_valid, scenario_checked), + } + summary: dict[str, Any] = { + "total": total, + "passed_count": passed_count, + "failed_count": total - passed_count, + "success_count": success_count, + "failure_count": total - success_count, + "tool_calls_finish_tool_calls": true_positive, + "tool_calls_finish_stop": expected_tool_finish_stop, + "stop_finish_tool_calls": false_positive, + "stop_finish_stop": expected_stop_finish_stop, + "expected_tool_call_total_count": len(labeled), + "tool_calls_successful_count": schema_successes, + "tool_calls_schema_validation_error_count": true_positive - schema_successes, + "error_only_reasoning_checked_count": total, + "error_only_reasoning_count": reasoning_errors, + "language_following_checked_count": language_checked, + "language_following_valid_count": language_valid, + "language_following_invalid_count": language_checked - language_valid, + "scenario_check_checked_count": scenario_checked, + "scenario_check_valid_count": scenario_valid, + "scenario_check_invalid_count": scenario_checked - scenario_valid, + "overall_compatibility_score": _ratio(passed_count, len(EXPECTED_INDICES)), + } + return summary, metrics + + +def _native_report( + *, + model: str, + endpoint: str | None, + fixture_metadata: Mapping[str, Any] | None, + results: list[dict[str, Any]], + completed: bool, + integration_error: BaseException | None = None, +) -> dict[str, Any]: + summary, metrics = _summarize(results) + report: dict[str, Any] = { + "verifier": ADAPTER_NAME, + "task": TASK_NAME, + "model": model, + "endpoint": endpoint, + "completed": completed, + "threshold": 1.0, + "sampling": {"temperature": 0, "top_p": 1, "max_tokens": 2048}, + "source": { + "url": (fixture_metadata or {}).get("source", UPSTREAM_SOURCE), + "ref": (fixture_metadata or {}).get("ref", UPSTREAM_REF), + "indices": list(EXPECTED_INDICES), + }, + "summary": summary, + "metrics": metrics, + "results": results, + } + if integration_error is not None: + report["integration_error"] = _error_dict(integration_error) + return report + + +def _compatibility_result( + model: str, + score: float, + *, + n_samples: int, + integration_error: BaseException | None = None, +) -> dict[str, Any]: + result: dict[str, Any] = { + "result_format": RESULT_FORMAT, + "eval_adapter": ADAPTER_NAME, + "model_name": model, + "results": { + TASK_NAME: { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.0, + } + }, + "configs": { + TASK_NAME: { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], + } + }, + "n-samples": { + TASK_NAME: { + "original": len(EXPECTED_INDICES), + "effective": n_samples, + } + }, + } + if integration_error is not None: + result["integration_error"] = _error_dict(integration_error) + return result + + +def prepare_compatibility_path(output_dir: Path) -> Path: + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") + return output_dir / f"results_minimax_vendor_{timestamp}.json" + + +def _write_json(path: Path, value: Mapping[str, Any]) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def _integration_results(exc: BaseException) -> list[dict[str, Any]]: + return [ + { + "data_index": data_index, + "status": "failed", + "attempts": 0, + "expected_tool_call": True + if data_index == 71 + else False + if data_index == 101 + else None, + "finish_reason": None, + "response": {"error": _error_dict(exc)}, + "error_only_reasoning_checked": 1, + "error_only_reasoning": False, + "case_passed": False, + "failures": ["integration_error"], + "suite_timed_out": isinstance(exc, SuiteTimeoutError), + } + for data_index in EXPECTED_INDICES + ] + + +def _failed_case_result(row: Mapping[str, Any], exc: BaseException) -> dict[str, Any]: + return { + "data_index": row["data_index"], + "status": "failed", + "attempts": 0, + "expected_tool_call": row.get("expected_tool_call"), + "finish_reason": None, + "response": {"error": _error_dict(exc)}, + "error_only_reasoning_checked": 1, + "error_only_reasoning": False, + "case_passed": False, + "failures": ["adapter_error"], + "suite_timed_out": isinstance(exc, SuiteTimeoutError), + } + + +def publish_integration_error( + *, output_dir: Path, model: str, error: BaseException +) -> None: + """Publish both required zero-score artifacts without loading jsonschema.""" + output_dir.mkdir(parents=True, exist_ok=True) + native_path = output_dir / NATIVE_REPORT_FILENAME + native_path.unlink(missing_ok=True) + compatibility_path = prepare_compatibility_path(output_dir) + _write_json( + native_path, + _native_report( + model=model, + endpoint=None, + fixture_metadata=None, + results=_integration_results(error), + completed=False, + integration_error=error, + ), + ) + _write_json( + compatibility_path, + _compatibility_result(model, 0.0, n_samples=0, integration_error=error), + ) + + +def run_evaluation( + *, + base_url: str, + api_key: str, + model: str, + output_dir: Path, + fixture_path: Path = DEFAULT_FIXTURE_PATH, + request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, + timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, + http_post: HttpPost = _default_http_post, + clock: Clock = time.monotonic, +) -> bool: + """Run all three cases sequentially and always publish both artifacts.""" + output_dir.mkdir(parents=True, exist_ok=True) + native_path = output_dir / NATIVE_REPORT_FILENAME + native_path.unlink(missing_ok=True) + compatibility_path = prepare_compatibility_path(output_dir) + + try: + request_timeout = _positive_number( + request_timeout_seconds, "request_timeout_seconds" + ) + suite_timeout = _positive_number(timeout_seconds, "timeout_seconds") + if not callable(http_post) or not callable(clock): + raise TypeError("http_post and clock must be callable") + deadline = clock() + suite_timeout + if not isinstance(model, str) or not model.strip(): + raise ValueError("model must be a non-empty string") + if not isinstance(api_key, str) or not api_key: + raise ValueError("api_key must be a non-empty string") + endpoint = build_endpoint(base_url) + fixture_metadata, rows = load_fixture(fixture_path) + except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + _write_json( + native_path, + _native_report( + model=model, + endpoint=None, + fixture_metadata=None, + results=_integration_results(exc), + completed=False, + integration_error=exc, + ), + ) + _write_json( + compatibility_path, + _compatibility_result(model, 0.0, n_samples=0, integration_error=exc), + ) + return False + + results: list[dict[str, Any]] = [] + for row in rows: + try: + result = _evaluate_case( + row=row, + model=model, + endpoint=endpoint, + api_key=api_key, + request_timeout_seconds=request_timeout, + deadline=deadline, + http_post=http_post, + clock=clock, + ) + except Exception as exc: # noqa: BLE001 - continue and report every case + result = _failed_case_result(row, exc) + results.append(result) + + timed_out = any(result["suite_timed_out"] for result in results) + integration_error: BaseException | None = None + completed = not timed_out + if timed_out: + integration_error = SuiteTimeoutError("global suite timeout exceeded") + native = _native_report( + model=model, + endpoint=endpoint, + fixture_metadata=fixture_metadata, + results=results, + completed=completed, + integration_error=integration_error, + ) + passed_count = native["summary"]["passed_count"] + effective = sum(not result["suite_timed_out"] for result in results) + score = passed_count / len(EXPECTED_INDICES) if completed else 0.0 + compatibility = _compatibility_result( + model, + score, + n_samples=effective, + integration_error=integration_error, + ) + _write_json(native_path, native) + _write_json(compatibility_path, compatibility) + return completed and passed_count == len(EXPECTED_INDICES) + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run the pinned three-case MiniMax M3 provider smoke." + ) + parser.add_argument("--base-url") + parser.add_argument("--api-key", default="EMPTY") + parser.add_argument("--model", required=True) + parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE_PATH) + parser.add_argument( + "--request-timeout-seconds", + type=_positive_float, + default=DEFAULT_REQUEST_TIMEOUT_SECONDS, + ) + parser.add_argument( + "--timeout-seconds", type=_positive_float, default=DEFAULT_TIMEOUT_SECONDS + ) + parser.add_argument("--integration-error") + args = parser.parse_args(argv) + if args.integration_error is None and args.base_url is None: + parser.error("--base-url required unless --integration-error is provided") + return args + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + if args.integration_error is not None: + publish_integration_error( + output_dir=args.output_dir, + model=args.model, + error=RuntimeError(args.integration_error), + ) + return 0 + passed = run_evaluation( + base_url=args.base_url, + api_key=args.api_key, + model=args.model, + output_dir=args.output_dir, + fixture_path=args.fixture, + request_timeout_seconds=args.request_timeout_seconds, + timeout_seconds=args.timeout_seconds, + ) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py new file mode 100644 index 0000000000..9ec39418b8 --- /dev/null +++ b/utils/evals/test_minimax_provider_eval.py @@ -0,0 +1,756 @@ +import builtins +import io +import json +import re +import sys +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any, Self + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import minimax_provider_eval as mpe + + +def _language_response(content: str = "お正月に子どもへ渡します。") -> dict[str, Any]: + return { + "id": "language-response", + "choices": [ + { + "finish_reason": "stop", + "message": {"content": content, "reasoning": ""}, + } + ], + } + + +def _tool_response( + *, arguments: str | None = None, finish_reason: str = "tool_calls" +) -> dict[str, Any]: + if finish_reason != "tool_calls": + return { + "id": "tool-response", + "choices": [ + {"finish_reason": finish_reason, "message": {"content": "done"}} + ], + } + if arguments is None: + arguments = json.dumps( + { + "patient_id": "P-009417", + "med_list_path": "/mnt/clinical/emr/patients/P-009417/med_list_v3.json", + "classification_scheme": "rxnorm_ingredient", + "overlap_policy": "current_only", + "strict_route_matching": True, + "include_otc": True, + "ignore_statuses": ["discontinued", "on_hold"], + "output_format": "detailed_json", + } + ) + return { + "id": "tool-response", + "choices": [ + { + "finish_reason": "tool_calls", + "message": { + "content": None, + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "flag_duplicate_therapies", + "arguments": arguments, + }, + } + ], + }, + } + ], + } + + +def _scenario_response( + content: str = "123, some-parameter, xyz, another-parameter", +) -> dict[str, Any]: + return { + "id": "scenario-response", + "choices": [{"finish_reason": "stop", "message": {"content": content}}], + } + + +def _response_for(payload: dict[str, Any]) -> dict[str, Any]: + tools = payload.get("tools", []) + if not tools: + return _language_response() + tool_name = tools[0]["function"]["name"] + if tool_name == "flag_duplicate_therapies": + return _tool_response() + assert tool_name == "example" + return _scenario_response() + + +def _compatibility(output_dir: Path) -> dict[str, Any]: + paths = list(output_dir.glob(mpe.COMPATIBILITY_GLOB)) + assert len(paths) == 1 + assert re.fullmatch( + r"results_minimax_vendor_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", + paths[0].name, + ) + return json.loads(paths[0].read_text(encoding="utf-8")) + + +def _native(output_dir: Path) -> dict[str, Any]: + return json.loads( + (output_dir / mpe.NATIVE_REPORT_FILENAME).read_text(encoding="utf-8") + ) + + +def _score(output_dir: Path) -> float: + return _compatibility(output_dir)["results"][mpe.TASK_NAME][ + "exact_match,strict-match" + ] + + +def _run( + output_dir: Path, + post: Any = _response_for, + **kwargs: Any, +) -> bool: + def http_post(**request: Any) -> Any: + return post(request["payload"]) + + return mpe.run_evaluation( + base_url="http://127.0.0.1:8000/v1/", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + http_post=http_post, + **kwargs, + ) + + +def test_success_writes_complete_native_and_compatibility_reports( + tmp_path: Path, +) -> None: + invocations: list[dict[str, Any]] = [] + + def http_post(**request: Any) -> dict[str, Any]: + invocations.append(request) + return _response_for(request["payload"]) + + output_dir = tmp_path / "output" + assert mpe.run_evaluation( + base_url="http://127.0.0.1:8000/v1/", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + http_post=http_post, + ) + + assert len(invocations) == 3 + assert [ + call["payload"].get("tools", [{}])[0].get("function", {}).get("name") + if call["payload"].get("tools") + else None + for call in invocations + ] == [None, "flag_duplicate_therapies", "example"] + for call in invocations: + assert call["url"] == "http://127.0.0.1:8000/v1/chat/completions" + assert call["headers"]["Authorization"] == "Bearer secret" + assert call["timeout_seconds"] <= mpe.DEFAULT_REQUEST_TIMEOUT_SECONDS + assert call["payload"]["model"] == "MiniMax-M3" + assert call["payload"]["temperature"] == 0 + assert call["payload"]["top_p"] == 1 + assert call["payload"]["max_tokens"] == 2048 + assert "data_index" not in call["payload"] + assert "check_type" not in call["payload"] + assert "expected_tool_call" not in call["payload"] + + native = _native(output_dir) + assert native["verifier"] == mpe.ADAPTER_NAME + assert native["task"] == mpe.TASK_NAME + assert native["completed"] is True + assert native["threshold"] == 1.0 + assert native["source"] == { + "url": mpe.UPSTREAM_SOURCE, + "ref": mpe.UPSTREAM_REF, + "indices": [0, 71, 101], + } + assert native["summary"]["total"] == 3 + assert native["summary"]["passed_count"] == 3 + assert native["summary"]["overall_compatibility_score"] == 1.0 + assert native["metrics"] == { + "Query-Success-Rate": 1.0, + "ToolCalls-Trigger-Similarity": 1.0, + "ToolCalls-Schema-Accuracy": 1.0, + "Error-Only-Reasoning-Rate": 0.0, + "Language-Following-Success-Rate": 1.0, + "Scenario-Check-Pass-Rate": 1.0, + } + assert [result["data_index"] for result in native["results"]] == [0, 71, 101] + assert [result["response"]["id"] for result in native["results"]] == [ + "language-response", + "tool-response", + "scenario-response", + ] + + compatibility = _compatibility(output_dir) + assert compatibility["result_format"] == mpe.RESULT_FORMAT + assert compatibility["eval_adapter"] == mpe.ADAPTER_NAME + assert compatibility["model_name"] == "MiniMax-M3" + assert _score(output_dir) == 1.0 + assert compatibility["n-samples"][mpe.TASK_NAME] == { + "original": 3, + "effective": 3, + } + assert "secret" not in json.dumps([native, compatibility]) + + +def test_schema_failure_fails_only_tool_case(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + if payload.get("tools", [{}])[0].get("function", {}).get("name") == ( + "flag_duplicate_therapies" + ): + return _tool_response(arguments="{}") + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + tool_result = native["results"][1] + assert tool_result["tool_calls_valid"] is False + assert tool_result["failures"] == ["tool_call_schema"] + assert native["metrics"]["ToolCalls-Schema-Accuracy"] == 0.0 + assert native["summary"]["passed_count"] == 2 + assert _score(output_dir) == pytest.approx(2 / 3) + + +def test_trigger_failure_uses_expected_label(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + if payload.get("tools", [{}])[0].get("function", {}).get("name") == ( + "flag_duplicate_therapies" + ): + return _tool_response(finish_reason="stop") + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + assert native["results"][1]["failures"] == ["tool_call_trigger"] + assert native["metrics"]["ToolCalls-Trigger-Similarity"] == 0.0 + assert native["summary"]["tool_calls_finish_stop"] == 1 + assert native["summary"]["stop_finish_stop"] == 1 + + +def test_negative_trigger_requires_stop_finish_reason(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + tools = payload.get("tools", []) + if tools and tools[0]["function"]["name"] == "example": + response = _scenario_response() + response["choices"][0]["finish_reason"] = "length" + return response + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + assert native["results"][2]["failures"] == ["tool_call_trigger"] + assert native["summary"]["stop_finish_stop"] == 0 + assert native["metrics"]["ToolCalls-Trigger-Similarity"] == 1.0 + + +def test_language_failure_uses_pinned_cyrillic_range(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + if not payload.get("tools"): + return _language_response("Это ответ") + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + assert native["results"][0]["language_following_checked"] is True + assert native["results"][0]["language_following_valid"] is False + assert native["results"][0]["failures"] == ["language_following"] + assert native["metrics"]["Language-Following-Success-Rate"] == 0.0 + + +def test_scenario_failure_uses_visible_first_occurrence_order(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + tools = payload.get("tools", []) + if tools and tools[0]["function"]["name"] == "example": + return _scenario_response( + "123 some-parameter xyz then some-parameter" + ) + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + result = _native(output_dir)["results"][2] + assert result["scenario_check_checked"] is True + assert result["scenario_check_detail"] == { + "expected": ["123", "some-parameter", "xyz", "another-parameter"], + "actual": ["xyz", "some-parameter"], + } + assert result["scenario_check_valid"] is False + assert result["failures"] == ["scenario_check"] + + +def test_transport_retries_once_then_preserves_success(tmp_path: Path) -> None: + attempts = 0 + + def http_post(**request: Any) -> dict[str, Any]: + nonlocal attempts + attempts += 1 + if attempts == 1: + raise OSError("connection reset") + return _response_for(request["payload"]) + + output_dir = tmp_path / "output" + assert mpe.run_evaluation( + base_url="https://provider.example/v1", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + http_post=http_post, + ) + assert attempts == 4 + assert _native(output_dir)["results"][0]["attempts"] == 2 + assert _score(output_dir) == 1.0 + + +@pytest.mark.parametrize( + ("status_code", "error_type"), + ( + (400, ValueError), + (408, ValueError), + (429, mpe.TransportError), + (503, mpe.TransportError), + ), +) +def test_default_http_post_retries_only_retryable_http_statuses( + monkeypatch: pytest.MonkeyPatch, + status_code: int, + error_type: type[BaseException], +) -> None: + def fail_request(*args: Any, **kwargs: Any) -> Any: + raise mpe.urllib.error.HTTPError( + "https://provider.example/v1/chat/completions", + status_code, + "request failed", + {}, + io.BytesIO(b"provider rejected request"), + ) + + monkeypatch.setattr(mpe._NO_REDIRECT_OPENER, "open", fail_request) + + with pytest.raises(error_type): + mpe._default_http_post( + url="https://provider.example/v1/chat/completions", + headers={"Authorization": "Bearer secret"}, + payload={"model": "MiniMax-M3", "messages": []}, + timeout_seconds=1, + ) + + +def test_default_http_post_rejects_redirect_without_leaking_authorization() -> None: + received_authorization: list[str | None] = [] + + class TargetHandler(BaseHTTPRequestHandler): + def record_request(self) -> None: + received_authorization.append(self.headers.get("Authorization")) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(b'{"choices":[]}') + + do_GET = record_request + do_POST = record_request + + def log_message(self, *args: Any) -> None: + return None + + target_server = ThreadingHTTPServer(("127.0.0.1", 0), TargetHandler) + target_url = f"http://127.0.0.1:{target_server.server_address[1]}/credential-target" + + class RedirectHandler(BaseHTTPRequestHandler): + def do_POST(self) -> None: + self.send_response(302) + self.send_header("Location", target_url) + self.end_headers() + + def log_message(self, *args: Any) -> None: + return None + + redirect_server = ThreadingHTTPServer(("127.0.0.1", 0), RedirectHandler) + target_thread = threading.Thread(target=target_server.serve_forever, daemon=True) + redirect_thread = threading.Thread( + target=redirect_server.serve_forever, + daemon=True, + ) + target_thread.start() + redirect_thread.start() + try: + with pytest.raises(ValueError, match="HTTP 302"): + mpe._default_http_post( + url=( + f"http://127.0.0.1:{redirect_server.server_address[1]}" + "/v1/chat/completions" + ), + headers={"Authorization": "Bearer secret"}, + payload={"model": "MiniMax-M3", "messages": []}, + timeout_seconds=1, + ) + finally: + redirect_server.shutdown() + target_server.shutdown() + redirect_server.server_close() + target_server.server_close() + redirect_thread.join() + target_thread.join() + + assert received_authorization == [] + + +@pytest.mark.parametrize( + "malformed", + ( + None, + [], + {}, + {"choices": []}, + {"choices": [{}]}, + {"choices": [{"finish_reason": "stop"}]}, + { + "choices": [ + {"finish_reason": "stop", "message": {"content": {"not": "text"}}} + ] + }, + {"choices": [{"finish_reason": "tool_calls", "message": {"tool_calls": {}}}]}, + { + "choices": [ + { + "finish_reason": "stop", + "message": { + "content": "123, some-parameter, xyz, another-parameter", + "tool_calls": [{"id": "unexpected"}], + }, + } + ] + }, + ), +) +def test_malformed_chat_response_records_diagnostic_and_continues( + tmp_path: Path, malformed: Any +) -> None: + calls = 0 + + def post(payload: dict[str, Any]) -> Any: + nonlocal calls + calls += 1 + return malformed + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + assert calls == 3 + assert len(native["results"]) == 3 + for result in native["results"]: + assert result["status"] == "failed" + assert result["failures"][0] == "query_failed" + assert result["response"]["error"]["type"] in {"TypeError", "ValueError"} + + +def test_exhausted_transport_failure_does_not_skip_later_cases(tmp_path: Path) -> None: + calls = 0 + + def http_post(**request: Any) -> dict[str, Any]: + nonlocal calls + calls += 1 + if calls <= 2: + raise mpe.TransportError("offline") + return _response_for(request["payload"]) + + output_dir = tmp_path / "output" + assert not mpe.run_evaluation( + base_url="https://provider.example/v1", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + http_post=http_post, + ) + native = _native(output_dir) + assert calls == 4 + assert len(native["results"]) == 3 + assert native["results"][0]["failures"] == [ + "query_failed", + "language_following", + ] + assert native["results"][1]["case_passed"] is True + assert native["results"][2]["case_passed"] is True + assert native["metrics"]["Query-Success-Rate"] == pytest.approx(2 / 3) + + +def test_global_deadline_caps_attempts_and_publishes_partial_report( + tmp_path: Path, +) -> None: + now = [0.0] + timeouts: list[float] = [] + + def clock() -> float: + return now[0] + + def http_post(**request: Any) -> dict[str, Any]: + timeouts.append(request["timeout_seconds"]) + now[0] += 0.7 + return _response_for(request["payload"]) + + output_dir = tmp_path / "output" + assert not mpe.run_evaluation( + base_url="https://provider.example/v1", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + timeout_seconds=1.0, + request_timeout_seconds=180, + http_post=http_post, + clock=clock, + ) + native = _native(output_dir) + assert timeouts == pytest.approx([1.0, 0.3]) + assert len(native["results"]) == 3 + assert native["results"][0]["case_passed"] is True + assert native["results"][1]["suite_timed_out"] is True + assert native["results"][2]["suite_timed_out"] is True + assert native["completed"] is False + assert native["integration_error"]["type"] == "SuiteTimeoutError" + compatibility = _compatibility(output_dir) + assert _score(output_dir) == 0.0 + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 1 + assert compatibility["integration_error"]["type"] == "SuiteTimeoutError" + + +def test_default_http_post_bounds_a_drip_feed_body_by_wall_clock( + monkeypatch: pytest.MonkeyPatch, +) -> None: + now = [0.0] + + class FakeSocket: + def __init__(self) -> None: + self.timeouts: list[float] = [] + + def settimeout(self, timeout: float) -> None: + self.timeouts.append(timeout) + + class DripResponse: + def __init__(self) -> None: + self.socket = FakeSocket() + self.fp = type( + "Raw", (), {"raw": type("Socket", (), {"_sock": self.socket})()} + )() + self.reads = 0 + + def __enter__(self) -> Self: + return self + + def __exit__(self, *args: object) -> None: + return None + + def read(self, size: int) -> bytes: + return self.read1(size) + + def read1(self, size: int) -> bytes: + self.reads += 1 + now[0] += 0.02 + return b"x" + + response = DripResponse() + monkeypatch.setattr(mpe.time, "monotonic", lambda: now[0]) + monkeypatch.setattr( + mpe._NO_REDIRECT_OPENER, + "open", + lambda *args, **kwargs: response, + ) + + with pytest.raises(mpe.TransportError, match="deadline"): + mpe._default_http_post( + url="https://provider.example/v1/chat/completions", + headers={"Authorization": "Bearer secret"}, + payload={"model": "MiniMax-M3", "messages": []}, + timeout_seconds=0.05, + ) + + assert response.reads == 3 + assert response.socket.timeouts == pytest.approx([0.05, 0.03, 0.01]) + + +def test_reasoning_only_response_is_always_checked(tmp_path: Path) -> None: + def post(payload: dict[str, Any]) -> dict[str, Any]: + if not payload.get("tools"): + return { + "choices": [ + { + "finish_reason": "length", + "message": { + "reasoning": "I could not answer", + "content": "", + "tool_calls": [], + }, + } + ] + } + return _response_for(payload) + + output_dir = tmp_path / "output" + assert not _run(output_dir, post) + native = _native(output_dir) + assert native["results"][0]["error_only_reasoning"] is True + assert native["results"][0]["failures"] == [ + "error_only_reasoning", + "language_following", + ] + assert native["metrics"]["Error-Only-Reasoning-Rate"] == pytest.approx(1 / 3) + + +def test_stale_artifacts_are_removed_without_touching_foreign_results( + tmp_path: Path, +) -> None: + output_dir = tmp_path / "output" + output_dir.mkdir() + (output_dir / mpe.NATIVE_REPORT_FILENAME).write_text("stale") + for stamp in ("2000-01-01T00-00-00.000000", "2001-01-01T00-00-00.000000"): + (output_dir / f"results_minimax_vendor_{stamp}.json").write_text("stale") + foreign = output_dir / "results_kimi_vendor_keep.json" + foreign.write_text("keep") + + assert _run(output_dir) + assert len(list(output_dir.glob(mpe.COMPATIBILITY_GLOB))) == 1 + assert _native(output_dir)["completed"] is True + assert foreign.read_text() == "keep" + + +def test_integration_error_cli_is_dependency_free_and_writes_both_artifacts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + output_dir = tmp_path / "output" + real_import = builtins.__import__ + + def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "jsonschema" or name.startswith("jsonschema."): + raise ImportError("jsonschema setup failed") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", guarded_import) + assert ( + mpe.main( + [ + "--model", + "MiniMax-M3", + "--output-dir", + str(output_dir), + "--integration-error", + "dependency installation failed", + ] + ) + == 0 + ) + + native = _native(output_dir) + assert native["completed"] is False + assert len(native["results"]) == 3 + assert native["integration_error"] == { + "type": "RuntimeError", + "message": "dependency installation failed", + } + compatibility = _compatibility(output_dir) + assert _score(output_dir) == 0.0 + assert compatibility["n-samples"][mpe.TASK_NAME] == { + "original": 3, + "effective": 0, + } + assert compatibility["integration_error"]["message"] == ( + "dependency installation failed" + ) + + +def test_invalid_runtime_input_writes_zero_score_artifacts(tmp_path: Path) -> None: + output_dir = tmp_path / "output" + called = False + + def http_post(**request: Any) -> Any: + nonlocal called + called = True + return _response_for(request["payload"]) + + assert not mpe.run_evaluation( + base_url="provider-without-a-scheme", + api_key="secret", + model="MiniMax-M3", + output_dir=output_dir, + http_post=http_post, + ) + assert called is False + assert _native(output_dir)["integration_error"]["type"] == "ValueError" + assert _score(output_dir) == 0.0 + assert _compatibility(output_dir)["n-samples"][mpe.TASK_NAME]["effective"] == 0 + + +@pytest.mark.parametrize( + "field", + ["indices", "ref", "license", "rows", "prompt", "tool_schema"], +) +def test_fixture_rejects_unpinned_or_incomplete_input( + tmp_path: Path, field: str +) -> None: + fixture = json.loads(mpe.DEFAULT_FIXTURE_PATH.read_text(encoding="utf-8")) + if field == "indices": + fixture[field] = [0, 71] + elif field == "ref": + fixture[field] = "main" + elif field == "license": + fixture[field] = "MIT License" + elif field == "rows": + fixture[field] = fixture[field][:-1] + elif field == "prompt": + fixture["rows"][0]["messages"][0]["content"] = "changed" + else: + fixture["rows"][1]["tools"][0]["function"]["parameters"]["type"] = "array" + path = tmp_path / "fixture.json" + path.write_text(json.dumps(fixture), encoding="utf-8") + + with pytest.raises(ValueError): + mpe.load_fixture(path) + + +def test_cli_validates_required_url_and_positive_bounds(tmp_path: Path) -> None: + with pytest.raises(SystemExit): + mpe.parse_args(["--model", "MiniMax-M3", "--output-dir", str(tmp_path)]) + with pytest.raises(SystemExit): + mpe.parse_args( + [ + "--model", + "MiniMax-M3", + "--output-dir", + str(tmp_path), + "--base-url", + "https://provider.example/v1", + "--timeout-seconds", + "0", + ] + ) + with pytest.raises(SystemExit): + mpe.parse_args( + [ + "--model", + "MiniMax-M3", + "--output-dir", + str(tmp_path), + "--base-url", + "https://provider.example/v1", + "--request-timeout-seconds", + "nan", + ] + ) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 8dacbebda4..5a4b504f54 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -17,6 +17,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] BENCHMARK_LIB = REPO_ROOT / "benchmarks" / "benchmark_lib.sh" +SINGLE_NODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-tmpl.yml" MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" QWEN_SGLANG_MTP_LAUNCHERS = ( @@ -29,6 +30,7 @@ run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } run_kimi_vendor_eval() { echo "DISPATCH=kimi-vendor"; } +run_minimax_vendor_eval() { echo "DISPATCH=minimax-vendor"; } append_lm_eval_summary() { echo "STAGED=summary"; } export EVAL_MAX_MODEL_LEN=16384 export EVAL_CONCURRENT_REQUESTS="" @@ -196,7 +198,7 @@ def test_run_eval_rejects_suite_override_for_lm_eval() -> None: ) assert result.returncode == 2 - assert "only supported with EVAL_FRAMEWORK=kimi-vendor" in result.stderr + assert "only supported with a provider verifier framework" in result.stderr def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: @@ -288,6 +290,283 @@ def test_kimi_vendor_rejects_unsupported_suite() -> None: assert "unsupported Kimi Vendor Verifier suite 'gsm8k'" in result.stderr +def _run_minimax_dispatch(*, suite: str | None = None, concurrency: str = "") -> str: + script = r''' +source "$BENCHMARK_LIB" +unset EVAL_MAX_MODEL_LEN +compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } +MINIMAX_DISPATCH_COUNT=0 +run_minimax_vendor_eval() { + MINIMAX_DISPATCH_COUNT=$((MINIMAX_DISPATCH_COUNT + 1)) + printf 'DISPATCH=minimax-vendor SUITE=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" +} +export EVAL_CONCURRENT_REQUESTS="$TEST_EVAL_CONCURRENCY" +export EVAL_ONLY=false +export IS_AGENTIC=0 +run_eval --framework minimax-vendor --port 9999 +printf 'DISPATCH_COUNT=%s\n' "$MINIMAX_DISPATCH_COUNT" +printf 'COMPLETED_SUITE=%s\n' "$EVAL_COMPLETED_SUITE" +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "MODEL": "served-model", + "MODEL_PREFIX": "minimaxm3", + "TEST_EVAL_CONCURRENCY": concurrency, + } + for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_COMPLETED_SUITE"): + env.pop(key, None) + if suite is not None: + env["EVAL_SUITE"] = suite + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout + return result.stdout + + +def test_minimax_vendor_defaults_suite_dispatches_once_and_records_completion() -> None: + output = _run_minimax_dispatch() + + assert "DISPATCH=minimax-vendor SUITE=minimax_m3_smoke" in output + assert "ARGS=<--port 9999>" in output + assert "DISPATCH_COUNT=1" in output + assert "COMPLETED_SUITE=minimax_m3_smoke" in output + + +def test_minimax_vendor_accepts_explicit_supported_suite() -> None: + output = _run_minimax_dispatch(suite="minimax_m3_smoke") + + assert "DISPATCH=minimax-vendor SUITE=minimax_m3_smoke" in output + assert "DISPATCH_COUNT=1" in output + assert "COMPLETED_SUITE=minimax_m3_smoke" in output + + +def test_minimax_vendor_rejects_unsupported_suite() -> None: + result = _run_invalid_call( + "MODEL_PREFIX=minimaxm3 " + "EVAL_SUITE=gsm8k " + "run_eval --framework minimax-vendor" + ) + + assert result.returncode == 2 + assert "unsupported MiniMax Provider Verifier suite 'gsm8k'" in result.stderr + + +def test_run_eval_rejects_unknown_framework() -> None: + result = _run_invalid_call( + "EVAL_MAX_MODEL_LEN=16384 run_eval --framework not-a-framework" + ) + + assert result.returncode == 1 + assert "Unknown framework 'not-a-framework'" in result.stdout + + +def test_minimax_vendor_rejects_concurrency_sweep_for_sequential_smoke() -> None: + result = _run_invalid_call( + "MODEL_PREFIX=minimaxm3 " + "EVAL_CONCURRENT_REQUESTS='1 4' " + "run_eval --framework minimax-vendor" + ) + + assert result.returncode == 1 + assert "batched eval concurrency is only supported for lm-eval" in result.stderr + + +def test_minimax_vendor_ignores_single_launcher_concurrency_value() -> None: + output = _run_minimax_dispatch(concurrency="128") + + assert "DISPATCH=minimax-vendor SUITE=minimax_m3_smoke" in output + assert "DISPATCH_COUNT=1" in output + + +def test_minimax_vendor_rejects_non_m3_model() -> None: + result = _run_invalid_call( + "MODEL=moonshotai/Kimi-K2 " + "MODEL_PREFIX=kimik3 " + "run_minimax_vendor_eval" + ) + + assert result.returncode == 2 + assert "requires MODEL_PREFIX=minimaxm3" in result.stderr + + +def test_minimax_vendor_accepts_case_insensitive_m3_model_name() -> None: + script = r''' +source "$BENCHMARK_LIB" +_run_minimax_m3_smoke_eval() { echo "DISPATCH=$EVAL_SUITE"; } +unset MODEL_PREFIX EVAL_SUITE EVAL_RESULT_DIR +MODEL_NAME=vendor/MINIMAX-M3-custom run_minimax_vendor_eval +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=True, + ) + + assert "DISPATCH=minimax_m3_smoke" in result.stdout + + +def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( + tmp_path: Path, +) -> None: + script = r''' +source "$BENCHMARK_LIB" +unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE +unset VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +_prepare_vendor_verifier_python() { return 12; } +_prepare_minimax_vendor_runtime() { echo "UNEXPECTED_DEPENDENCY_INSTALL"; return 99; } +python3() { printf 'ADAPTER_ARG=<%s>\n' "$@"; } +append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR"; } +export MODEL_PREFIX=minimaxm3 +export MODEL=test-model +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +run_eval --framework minimax-vendor --results-dir "$RESULTS_DIR" +eval_rc=$? +printf 'EVAL_RC=%s\n' "$eval_rc" +''' + results_dir = tmp_path / "results" + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + }, + text=True, + capture_output=True, + check=True, + ) + output = result.stdout + result.stderr + + assert "EVAL_RC=12" in output + assert "UNEXPECTED_DEPENDENCY_INSTALL" not in output + assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/minimax_provider_eval.py'}>" in output + assert "ADAPTER_ARG=" in output + assert f"ADAPTER_ARG=<{results_dir}>" in output + assert "ADAPTER_ARG=<--integration-error>" in output + assert ( + "ADAPTER_ARG=" + ) in output + assert f"STAGED=<{results_dir}>" in output + assert output.count("STAGED=<") == 1 + + +def test_minimax_vendor_dependency_install_is_pinned_and_minimal( + tmp_path: Path, +) -> None: + script = r''' +source "$BENCHMARK_LIB" +selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +VENDOR_VERIFIER_PYTHON=selected_python +_install_minimax_vendor_eval_deps "$RUNTIME_DIR" +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RUNTIME_DIR": str(tmp_path / "runtime"), + }, + text=True, + capture_output=True, + check=True, + ) + + assert "PYTHON_ARG=" in result.stdout + assert "PYTHON_ARG= None: + results_dir = tmp_path / "results" + runtime_dir = tmp_path / "runtime" + python_dir = tmp_path / "python" + script = r''' +source "$BENCHMARK_LIB" +selected_python() { + printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" >&2 + printf 'PYTHON_ARG=<%s>\n' "$@" >&2 +} +_prepare_vendor_verifier_python() { + mkdir "$PYTHON_DIR" + VENDOR_VERIFIER_PYTHON=selected_python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +} +_prepare_minimax_vendor_runtime() { + mkdir "$RUNTIME_DIR" + printf '%s\n' "$RUNTIME_DIR" +} +mktemp() { echo "UNEXPECTED_DEFAULT_RESULTS_DIR" >&2; return 99; } +run_minimax_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" +printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" +printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "RUNTIME_DIR": str(runtime_dir), + "PYTHON_DIR": str(python_dir), + "MODEL": "test-model", + "MODEL_PREFIX": "minimaxm3", + "OPENAI_API_KEY": "must-not-be-forwarded", + } + for key in ("EVAL_SUITE", "EVAL_RESULT_DIR", "MODEL_NAME"): + env.pop(key, None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + output = result.stdout + result.stderr + adapter = REPO_ROOT / "utils/evals/minimax_provider_eval.py" + fixture = REPO_ROOT / "utils/evals/minimax_m3_smoke.json" + + assert f"PYTHONPATH=<{runtime_dir}" in output + for value in ( + adapter, + "http://127.0.0.1:9999/v1", + "EMPTY", + "test-model", + results_dir, + fixture, + "180", + "900", + ): + assert f"PYTHON_ARG=<{value}>" in output + for option in ( + "--base-url", + "--api-key", + "--model", + "--output-dir", + "--fixture", + "--request-timeout-seconds", + "--timeout-seconds", + ): + assert f"PYTHON_ARG=<{option}>" in output + assert "must-not-be-forwarded" not in output + assert "UNEXPECTED_DEFAULT_RESULTS_DIR" not in output + assert "EVAL_SUITE=minimax_m3_smoke" in output + assert f"EVAL_RESULT_DIR={results_dir}" in output + assert not runtime_dir.exists() + assert not python_dir.exists() + + def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, @@ -296,11 +575,11 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( python_dir = tmp_path / "python" script = r''' source "$BENCHMARK_LIB" -_prepare_kimi_vendor_python() { +_prepare_vendor_verifier_python() { mkdir "$PYTHON_DIR" - KIMI_VENDOR_PYTHON=/unusable/bootstrap/python - KIMI_VENDOR_PYTHON_CLEANUP_DIR="$PYTHON_DIR" - export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR + VENDOR_VERIFIER_PYTHON=/unusable/bootstrap/python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR } _prepare_kimi_vendor_runtime() { return 12; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" @@ -547,11 +826,11 @@ def test_kimi_vendor_uses_system_python_fast_path() -> None: [ "$1" = "-c" ] } mktemp() { echo "UNEXPECTED_MKTEMP"; return 99; } -KIMI_VENDOR_PYTHON=/previous/python -KIMI_VENDOR_PYTHON_CLEANUP_DIR=/previous/runtime -_prepare_kimi_vendor_python -printf 'SELECTED_PYTHON=<%s>\n' "$KIMI_VENDOR_PYTHON" -printf 'PYTHON_CLEANUP=<%s>\n' "$KIMI_VENDOR_PYTHON_CLEANUP_DIR" +VENDOR_VERIFIER_PYTHON=/previous/python +VENDOR_VERIFIER_PYTHON_CLEANUP_DIR=/previous/runtime +_prepare_vendor_verifier_python "Kimi Vendor Verifier" "kimi-vendor-python" +printf 'SELECTED_PYTHON=<%s>\n' "$VENDOR_VERIFIER_PYTHON" +printf 'PYTHON_CLEANUP=<%s>\n' "$VENDOR_VERIFIER_PYTHON_CLEANUP_DIR" ''' result = subprocess.run( ["bash", "-c", script], @@ -607,14 +886,14 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( cp "$FAKE_UV" "$prefix/bin/uv" chmod +x "$prefix/bin/uv" } -_prepare_kimi_vendor_python -cleanup_dir="$KIMI_VENDOR_PYTHON_CLEANUP_DIR" -printf 'SELECTED_PYTHON=<%s>\n' "$KIMI_VENDOR_PYTHON" +_prepare_vendor_verifier_python "Kimi Vendor Verifier" "kimi-vendor-python" +cleanup_dir="$VENDOR_VERIFIER_PYTHON_CLEANUP_DIR" +printf 'SELECTED_PYTHON=<%s>\n' "$VENDOR_VERIFIER_PYTHON" printf 'PYTHON_CLEANUP=<%s>\n' "$cleanup_dir" runtime_dir="$TEST_ROOT/runtime" mkdir "$runtime_dir" _install_kimi_vendor_eval_deps "$runtime_dir" -_cleanup_kimi_vendor_eval "$runtime_dir" "$cleanup_dir" +_cleanup_vendor_eval "$runtime_dir" "$cleanup_dir" [ ! -e "$runtime_dir" ] && [ ! -e "$cleanup_dir" ] && printf 'CLEANED\n' ''' result = subprocess.run( @@ -654,7 +933,7 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: script = r''' source "$BENCHMARK_LIB" selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } -KIMI_VENDOR_PYTHON=selected_python +VENDOR_VERIFIER_PYTHON=selected_python _install_kimi_vendor_eval_deps "$RUNTIME_DIR" ''' result = subprocess.run( @@ -678,7 +957,7 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: def test_kimi_vendor_surfaces_failure_artifact_error(tmp_path: Path) -> None: script = r''' source "$BENCHMARK_LIB" -_prepare_kimi_vendor_python() { return 12; } +_prepare_vendor_verifier_python() { return 12; } _write_kimi_vendor_integration_error() { return 23; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'EVAL_RC=%s\n' "$?" @@ -713,11 +992,11 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( verifier_dir.mkdir() script = r''' source "$BENCHMARK_LIB" -_prepare_kimi_vendor_python() { +_prepare_vendor_verifier_python() { mkdir "$PYTHON_DIR" - KIMI_VENDOR_PYTHON=selected_python - KIMI_VENDOR_PYTHON_CLEANUP_DIR="$PYTHON_DIR" - export KIMI_VENDOR_PYTHON KIMI_VENDOR_PYTHON_CLEANUP_DIR + VENDOR_VERIFIER_PYTHON=selected_python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR } _prepare_kimi_vendor_runtime() { mkdir "$RUNTIME_DIR" @@ -726,7 +1005,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( } _prepare_kimi_vendor_verifier() { printf 'CHECKOUT=%s@%s\n' "$1" "$2" >&2 - "$KIMI_VENDOR_PYTHON" - "$1" "$2" "$VERIFIER_DIR" <<'PY' >&2 + "$VENDOR_VERIFIER_PYTHON" - "$1" "$2" "$VERIFIER_DIR" <<'PY' >&2 archive extraction PY printf '%s\n' "$VERIFIER_DIR" @@ -1722,6 +2001,19 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: "toJson(matrix.config['kv-offload-backend']) || '' }}" ) +def test_fixed_eval_workflows_forward_provider_contract() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + for job_name in ("test-sweep-evals", "test-sweep-multi-node-evals"): + forwarded = workflow["jobs"][job_name]["with"] + assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" + assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + + reusable_workflow = yaml.safe_load(SINGLE_NODE_WORKFLOW.read_text()) + assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" + assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" + assert "*_vendor_report.json" in SINGLE_NODE_WORKFLOW.read_text() + + def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index bb3a4f58b6..8adb806b50 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -2,6 +2,7 @@ default: gsm8k: 0.90 kimi_tool_call_schema: 1.0 + minimax_m3_smoke: 1.0 gpqa_diamond_cot_n_shot: 0.30 swebench_lite: 0.50 models: diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 0521d03073..de7a27b850 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -10,12 +10,17 @@ collect_eval_rows, ) from evals.kimi_vendor_eval import RESULT_FORMAT as KIMI_VENDOR_RESULT_FORMAT +from evals.minimax_provider_eval import RESULT_FORMAT as MINIMAX_RESULT_FORMAT def test_kimi_vendor_result_format_matches_collector_contract() -> None: assert KIMI_VENDOR_RESULT_FORMAT == EVAL_RESULT_FORMAT +def test_minimax_result_format_matches_collector_contract() -> None: + assert MINIMAX_RESULT_FORMAT == EVAL_RESULT_FORMAT + + def test_build_row_preserves_sequence_lengths() -> None: row = build_row( { @@ -136,24 +141,28 @@ def test_collect_eval_rows_ignores_failed_batch_points( -def test_collect_eval_rows_accepts_neutral_result_format(tmp_path: Path) -> None: - artifact_dir = tmp_path / "eval_provider" +def test_collect_eval_rows_accepts_minimax_compatibility_result( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_minimax" artifact_dir.mkdir() (artifact_dir / "meta_env.json").write_text( - json.dumps({"eval_suite": "provider_smoke"}) + json.dumps({"eval_suite": "minimax_m3_smoke"}) ) - result_path = artifact_dir / "results_provider.json" - _write_lm_eval_result(result_path, 1.0) + result_path = artifact_dir / "results_minimax_vendor.json" + _write_lm_eval_result(result_path, 1.0, task="minimax_m3_smoke") result = json.loads(result_path.read_text()) result.pop("lm_eval_version") result["result_format"] = EVAL_RESULT_FORMAT + result["eval_adapter"] = "minimax-provider-verifier" result_path.write_text(json.dumps(result)) rows = collect_eval_rows(tmp_path) assert len(rows) == 1 + assert rows[0]["task"] == "minimax_m3_smoke" assert rows[0]["score"] == 1.0 - assert rows[0]["eval_suite"] == "provider_smoke" + assert rows[0]["eval_suite"] == "minimax_m3_smoke" def test_collect_eval_rows_excludes_integration_and_sample_failures( From 301dcdf5e0fe8f2d79219b229d8673a7003e0c2f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:57:01 -0500 Subject: [PATCH 35/99] fix: preserve minimax m3 generation budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the pinned upstream 40960-token M3 default so valid tool-call responses do not truncate with finish_reason=length. 中文:采用上游固定版本为 M3 设置的 40960 token 默认值,避免有效工具调用响应因 finish_reason=length 被截断。 --- utils/evals/EVALS.md | 10 ++++++---- utils/evals/minimax_provider_eval.py | 9 +++++++-- utils/evals/test_minimax_provider_eval.py | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 9daae9800d..1582e795d2 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -160,10 +160,12 @@ The adapter sends the three requests sequentially to Bearer token and chat-completions request body and return OpenAI-compatible message, finish-reason, and tool-call fields. Redirect responses are rejected before forwarding bearer credentials; the local runner supplies -`Authorization: Bearer EMPTY`. The smoke overrides fixture -sampling with `temperature: 0`, `top_p: 1`, and `max_tokens: 2048`. Each request -has a 180-second timeout by default and at most one retry for transport -failures, HTTP 429, or HTTP 5xx responses (two total attempts); a hard +`Authorization: Bearer EMPTY`. The smoke overrides fixture sampling with +`temperature: 0`, `top_p: 1`, and the upstream M3 default +`max_tokens: 40960`, which prevents valid tool-call responses from ending at +the model's common 2048-token generation default. Each request has a +180-second timeout by default and at most one retry for transport failures, +HTTP 429, or HTTP 5xx responses (two total attempts); a hard 900-second global bound covers the whole suite. `minimax_vendor_report.json` is the native report. It preserves every raw diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index a8f6a82394..2adcfd9652 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -24,6 +24,7 @@ DEFAULT_FIXTURE_PATH = Path(__file__).with_name("minimax_m3_smoke.json") DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 DEFAULT_TIMEOUT_SECONDS = 900.0 +M3_DEFAULT_MAX_TOKENS = 40960 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "minimax-provider-verifier" EXPECTED_INDICES = (0, 71, 101) @@ -195,7 +196,7 @@ def prepare_request(row: Mapping[str, Any], model: str) -> dict[str, Any]: model=model, temperature=0, top_p=1, - max_tokens=2048, + max_tokens=M3_DEFAULT_MAX_TOKENS, ) return request @@ -785,7 +786,11 @@ def _native_report( "endpoint": endpoint, "completed": completed, "threshold": 1.0, - "sampling": {"temperature": 0, "top_p": 1, "max_tokens": 2048}, + "sampling": { + "temperature": 0, + "top_p": 1, + "max_tokens": M3_DEFAULT_MAX_TOKENS, + }, "source": { "url": (fixture_metadata or {}).get("source", UPSTREAM_SOURCE), "ref": (fixture_metadata or {}).get("ref", UPSTREAM_REF), diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 9ec39418b8..71f5c41260 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -165,7 +165,7 @@ def http_post(**request: Any) -> dict[str, Any]: assert call["payload"]["model"] == "MiniMax-M3" assert call["payload"]["temperature"] == 0 assert call["payload"]["top_p"] == 1 - assert call["payload"]["max_tokens"] == 2048 + assert call["payload"]["max_tokens"] == mpe.M3_DEFAULT_MAX_TOKENS assert "data_index" not in call["payload"] assert "check_type" not in call["payload"] assert "expected_tool_call" not in call["payload"] From 165135e1264a52a0da397ba359f493d9d2a36e55 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:09:33 -0500 Subject: [PATCH 36/99] fix: retry truncated minimax response bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate urllib HTTP protocol failures such as IncompleteRead into the existing one-retry transport path. 中文:将 IncompleteRead 等 urllib HTTP 协议异常转换为既有的传输失败类型,使截断响应按策略重试一次。 --- utils/evals/minimax_provider_eval.py | 8 +++++- utils/evals/test_minimax_provider_eval.py | 31 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 2adcfd9652..ac19933c25 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -6,6 +6,7 @@ import argparse import copy import hashlib +import http.client import json import math import re @@ -250,7 +251,12 @@ def _default_http_post( raise ValueError( f"chat completion request failed with HTTP {exc.code}: {exc.reason}" ) from exc - except (urllib.error.URLError, TimeoutError, OSError) as exc: + except ( + urllib.error.URLError, + http.client.HTTPException, + TimeoutError, + OSError, + ) as exc: raise TransportError(str(exc)) from exc try: return json.loads(content) diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 71f5c41260..e026aba2fb 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -356,6 +356,37 @@ def fail_request(*args: Any, **kwargs: Any) -> Any: ) +def test_default_http_post_maps_incomplete_body_to_transport_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class IncompleteResponse: + def __enter__(self) -> Self: + return self + + def __exit__(self, *args: object) -> None: + return None + + def read(self, size: int) -> bytes: + return self.read1(size) + + def read1(self, size: int) -> bytes: + raise mpe.http.client.IncompleteRead(b'{"choices":', 100) + + monkeypatch.setattr( + mpe._NO_REDIRECT_OPENER, + "open", + lambda *args, **kwargs: IncompleteResponse(), + ) + + with pytest.raises(mpe.TransportError): + mpe._default_http_post( + url="https://provider.example/v1/chat/completions", + headers={"Authorization": "Bearer secret"}, + payload={"model": "MiniMax-M3", "messages": []}, + timeout_seconds=1, + ) + + def test_default_http_post_rejects_redirect_without_leaking_authorization() -> None: received_authorization: list[str | None] = [] From ef08f9ba250bfd10c9e556c0f9ef792fdc14fe86 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:45:45 -0500 Subject: [PATCH 37/99] fix: narrow minimax smoke to schema case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将 MiniMax 冒烟评估收敛到稳定的工具调用模式检查,并允许在受支持模型上显式运行。 --- benchmarks/benchmark_lib.sh | 8 - utils/evals/EVALS.md | 83 +++++------ utils/evals/minimax_m3_smoke.json | 70 +-------- utils/evals/minimax_provider_eval.py | 18 +-- utils/evals/test_minimax_provider_eval.py | 173 +++++++++------------- utils/evals/test_run_eval_dispatch.py | 21 ++- 6 files changed, 129 insertions(+), 244 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 3912dcb7ec..979f154b82 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1439,14 +1439,6 @@ run_minimax_vendor_eval() { case "$eval_suite" in minimax_m3_smoke) - local model_name="${MODEL_NAME:-${MODEL:-}}" - local model_prefix="${MODEL_PREFIX:-}" - if [[ "$model_prefix" != [Mm][Ii][Nn][Ii][Mm][Aa][Xx][Mm]3 ]] \ - && [[ "$model_name" != *[Mm][Ii][Nn][Ii][Mm][Aa][Xx]-[Mm]3* ]]; then - echo "ERROR: MiniMax M3 smoke requires MODEL_PREFIX=minimaxm3 or a MODEL/MODEL_NAME containing MiniMax-M3" >&2 - export EVAL_RESULT_DIR="" - return 2 - fi _run_minimax_m3_smoke_eval "$@" ;; *) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 1582e795d2..3fe5a31f53 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -118,12 +118,10 @@ two-case smoke against their OpenAI-compatible frontend. Eval-only launchers restore real block verification before submitting recipes that otherwise use synthetic acceptance for throughput. -### MiniMax M3 provider compatibility smoke +### MiniMax provider compatibility smoke -The Phase 1 MiniMax smoke is opt-in and applies only to a MiniMax M3 provider -exposing an OpenAI-compatible chat-completions API. The runner rejects other -models unless `MODEL_PREFIX=minimaxm3` or `MODEL`/`MODEL_NAME` contains -`MiniMax-M3`. Select +The Phase 1 MiniMax smoke is opt-in and applies to supported models exposing +an OpenAI-compatible chat-completions API. Select `eval-framework: minimax-vendor` and `eval-suite: minimax_m3_smoke` in `e2e-tests.yml`, or run it from the repository root against an already-ready server: @@ -144,62 +142,49 @@ python3 utils/evals/validate_scores.py `sample.jsonl` at commit `85bf180e54e2ab0b31595cfdc697116c4760876d`. The vendored fixture retains the full upstream MIT copyright, permission, and warranty notice. It contains -exactly these three upstream zero-based rows, in this order: - -1. index 0, the non-Cyrillic language-following check; -2. index 71, an `expected_tool_call: true` request exercising tool-call trigger - and argument-schema validation; -3. index 101, an `expected_tool_call: false` request exercising the scenario - parameter key-order check. - -The adapter applies the pinned validator semantics directly to this fixture; it -does not download the upstream repository or run the remaining 99 cases. - -The adapter sends the three requests sequentially to -`${base_url}/chat/completions`. The endpoint must accept an OpenAI-compatible -Bearer token and chat-completions request body and return OpenAI-compatible -message, finish-reason, and tool-call fields. Redirect responses are rejected -before forwarding bearer credentials; the local runner supplies -`Authorization: Bearer EMPTY`. The smoke overrides fixture sampling with -`temperature: 0`, `top_p: 1`, and the upstream M3 default -`max_tokens: 40960`, which prevents valid tool-call responses from ending at -the model's common 2048-token generation default. Each request has a -180-second timeout by default and at most one retry for transport failures, -HTTP 429, or HTTP 5xx responses (two total attempts); a hard -900-second global bound covers the whole suite. - -`minimax_vendor_report.json` is the native report. It preserves every raw -response and reports the six requested upstream-derived metric families: - -- `Query-Success-Rate`, which records whether the endpoint returned a response, - not whether the answer was generally correct; -- `ToolCalls-Trigger-Similarity`, the F1 score computed from the fixture's - `expected_tool_call` labels; -- `ToolCalls-Schema-Accuracy`, which validates returned function names and - arguments against the requested tool schema; -- `Error-Only-Reasoning-Rate`, for responses with reasoning but neither visible - content nor tool calls; -- `Language-Following-Success-Rate`, the non-Cyrillic language check; -- `Scenario-Check-Pass-Rate`, the scenario parameter key-order check. +only upstream zero-based row 71, an `expected_tool_call: true` request +exercising tool-call trigger and argument-schema validation. The adapter +applies the pinned validator semantics directly to this fixture; it does not +download the upstream repository or run the remaining 101 cases. + +The adapter sends the request to `${base_url}/chat/completions`. The endpoint +must accept an OpenAI-compatible Bearer token and chat-completions request body +and return OpenAI-compatible message, finish-reason, and tool-call fields. +Redirect responses are rejected before forwarding bearer credentials; the +local runner supplies `Authorization: Bearer EMPTY`. The smoke uses +`temperature: 0`, `top_p: 1`, and `max_tokens: 40960`. The token budget matches +the pinned verifier's MiniMax M3 default and prevents a valid tool-call response +from ending at the model's common 2048-token generation default. The request +has a 180-second timeout by default and at most one retry for transport +failures, HTTP 429, or HTTP 5xx responses (two total attempts); a hard +900-second global bound covers the smoke. + +`minimax_vendor_report.json` is the native report. It preserves the raw +response and reports the six upstream-derived metric fields. This Phase 1 case +exercises `Query-Success-Rate`, `ToolCalls-Trigger-Similarity`, +`ToolCalls-Schema-Accuracy`, and `Error-Only-Reasoning-Rate`. +`Language-Following-Success-Rate` and `Scenario-Check-Pass-Rate` have no +applicable case in this fixture and report `0.0` with zero checked counts. The adapter additionally writes exactly one timestamped `results_minimax_vendor_*.json` compatibility artifact. Its `result_format` is `inferencex-eval-v1`, `eval_adapter` is `minimax-provider-verifier`, task is `minimax_m3_smoke`, and primary metric is `exact_match,strict-match`. A -completed run records original and effective sample counts of three. Its -compatibility score is passed cases divided by -three; the `minimax_m3_smoke` threshold is `1.0`, so every case must pass. +completed run records original and effective sample counts of one. Its score +is `1.0` only when row 71 returns a `tool_calls` finish reason and every +function call validates against the requested schema. The +`minimax_m3_smoke` threshold remains `1.0`. Both artifacts match the workflows' existing `results*.json` and `*_vendor_report.json` upload patterns. Setup, integration, timeout, and collection failures still emit a zero-score compatibility artifact with error metadata. -This is a fixed three-case provider compatibility smoke, not the full +This is a fixed single-case provider compatibility smoke, not the full 102-case MiniMax Provider Verifier, BFCL, or a cross-model quality comparison. It does not estimate the upstream dataset's aggregate rates, stochastic pass-at-k behavior, streaming behavior, parallel-call behavior, multi-turn tool -execution, or general agent quality. Its metric denominators are only the -applicable cases in this pinned three-row fixture. +execution, language following, scenario key-order recall, or general agent +quality. ### Benchmark script flow @@ -231,7 +216,7 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_eval` | Unified entrypoint - dispatches to framework-specific runner | | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | -| `run_minimax_vendor_eval` | Validates MiniMax M3 applicability and runs the pinned three-case provider smoke | +| `run_minimax_vendor_eval` | Runs the pinned single-case MiniMax provider smoke | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_prepare_vendor_verifier_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime for provider verifiers | diff --git a/utils/evals/minimax_m3_smoke.json b/utils/evals/minimax_m3_smoke.json index 9826a361da..4ff3863a7d 100644 --- a/utils/evals/minimax_m3_smoke.json +++ b/utils/evals/minimax_m3_smoke.json @@ -2,24 +2,10 @@ "source": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/85bf180e54e2ab0b31595cfdc697116c4760876d/sample.jsonl", "ref": "85bf180e54e2ab0b31595cfdc697116c4760876d", "indices": [ - 0, - 71, - 101 + 71 ], "license": "MIT License\n\nCopyright (c) 2025 MiniMax Provider Verifier Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", "rows": [ - { - "data_index": 0, - "messages": [ - { - "role": "user", - "content": "日本ではどのような時にお年玉を渡しますか?" - } - ], - "check_type": [ - "contains_russian_characters_unicode" - ] - }, { "data_index": 71, "messages": [ @@ -109,60 +95,6 @@ } ], "expected_tool_call": true - }, - { - "data_index": 101, - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "你好, 按看到的原始顺序复述一下 example 工具的参数, 注意就用看到的原始顺序、不要自行排序." - } - ], - "tools": [ - { - "type": "function", - "function": { - "name": "example", - "parameters": { - "type": "object", - "properties": { - "123": { - "type": "string", - "description": "..." - }, - "some-parameter": { - "type": "string", - "description": "..." - }, - "xyz": { - "type": "string", - "description": "..." - }, - "another-parameter": { - "type": "string", - "description": "..." - } - }, - "required": [ - "some-parameter", - "xyz", - "123", - "another-parameter" - ] - } - } - } - ], - "temperature": 1, - "top_p": 0.95, - "check_type": [ - "scenario_check" - ], - "expected_tool_call": false } ] } diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index ac19933c25..ecd937a2b9 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Run the pinned three-case MiniMax M3 provider compatibility smoke.""" +"""Run the pinned single-case MiniMax M3 provider compatibility smoke.""" from __future__ import annotations @@ -28,7 +28,7 @@ M3_DEFAULT_MAX_TOKENS = 40960 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "minimax-provider-verifier" -EXPECTED_INDICES = (0, 71, 101) +EXPECTED_INDICES = (71,) UPSTREAM_REF = "85bf180e54e2ab0b31595cfdc697116c4760876d" UPSTREAM_SOURCE = ( "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/" @@ -38,9 +38,7 @@ "aa7cec386fcb5e555aba0e8b1c31307940af41967708c9bc0f78b4e02e235dd5" ) EXPECTED_CASE_SHA256 = { - 0: "655d3135fc553b08c376f363165699548396428136fd9536345cf37b564b357a", 71: "10272004ae08f4a7d08d2306404f6cbb7bbfa794230e1082a235ded036d550ed", - 101: "10c3c2bf8d4e43d520de8ef3955cda1dcdd852c9c904decbc7d8cd040431afd4", } MAX_RESPONSE_BYTES = 16 * 1024 * 1024 MAX_ATTEMPTS = 2 @@ -120,12 +118,12 @@ def _validate_tools(value: Any, name: str) -> None: def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """Load and validate the exact pinned three-case fixture.""" + """Load and validate the exact pinned single-case fixture.""" root = _mapping(json.loads(path.read_text(encoding="utf-8")), "fixture") if root.get("source") != UPSTREAM_SOURCE or root.get("ref") != UPSTREAM_REF: raise ValueError("fixture source or ref does not match the pinned upstream") if root.get("indices") != list(EXPECTED_INDICES): - raise ValueError("fixture indices must be exactly [0, 71, 101]") + raise ValueError("fixture indices must be exactly [71]") license_text = root.get("license") if ( not isinstance(license_text, str) @@ -135,13 +133,11 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: raw_rows = root.get("rows") if not isinstance(raw_rows, list) or len(raw_rows) != len(EXPECTED_INDICES): - raise ValueError("fixture must contain exactly three rows") + raise ValueError("fixture must contain exactly one row") rows: list[dict[str, Any]] = [] expected_checks = { - 0: ["contains_russian_characters_unicode"], 71: [], - 101: ["scenario_check"], } for position, raw_row in enumerate(raw_rows): row = dict(_mapping(raw_row, f"fixture.rows[{position}]")) @@ -935,7 +931,7 @@ def run_evaluation( http_post: HttpPost = _default_http_post, clock: Clock = time.monotonic, ) -> bool: - """Run all three cases sequentially and always publish both artifacts.""" + """Run the pinned case and always publish both artifacts.""" output_dir.mkdir(parents=True, exist_ok=True) native_path = output_dir / NATIVE_REPORT_FILENAME native_path.unlink(missing_ok=True) @@ -1019,7 +1015,7 @@ def run_evaluation( def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Run the pinned three-case MiniMax M3 provider smoke." + description="Run the pinned single-case MiniMax M3 provider smoke." ) parser.add_argument("--base-url") parser.add_argument("--api-key", default="EMPTY") diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index e026aba2fb..61893773b2 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -151,13 +151,11 @@ def http_post(**request: Any) -> dict[str, Any]: http_post=http_post, ) - assert len(invocations) == 3 + assert len(invocations) == 1 assert [ call["payload"].get("tools", [{}])[0].get("function", {}).get("name") - if call["payload"].get("tools") - else None for call in invocations - ] == [None, "flag_duplicate_therapies", "example"] + ] == ["flag_duplicate_therapies"] for call in invocations: assert call["url"] == "http://127.0.0.1:8000/v1/chat/completions" assert call["headers"]["Authorization"] == "Bearer secret" @@ -178,24 +176,22 @@ def http_post(**request: Any) -> dict[str, Any]: assert native["source"] == { "url": mpe.UPSTREAM_SOURCE, "ref": mpe.UPSTREAM_REF, - "indices": [0, 71, 101], + "indices": [71], } - assert native["summary"]["total"] == 3 - assert native["summary"]["passed_count"] == 3 + assert native["summary"]["total"] == 1 + assert native["summary"]["passed_count"] == 1 assert native["summary"]["overall_compatibility_score"] == 1.0 assert native["metrics"] == { "Query-Success-Rate": 1.0, "ToolCalls-Trigger-Similarity": 1.0, "ToolCalls-Schema-Accuracy": 1.0, "Error-Only-Reasoning-Rate": 0.0, - "Language-Following-Success-Rate": 1.0, - "Scenario-Check-Pass-Rate": 1.0, + "Language-Following-Success-Rate": 0.0, + "Scenario-Check-Pass-Rate": 0.0, } - assert [result["data_index"] for result in native["results"]] == [0, 71, 101] + assert [result["data_index"] for result in native["results"]] == [71] assert [result["response"]["id"] for result in native["results"]] == [ - "language-response", "tool-response", - "scenario-response", ] compatibility = _compatibility(output_dir) @@ -204,8 +200,8 @@ def http_post(**request: Any) -> dict[str, Any]: assert compatibility["model_name"] == "MiniMax-M3" assert _score(output_dir) == 1.0 assert compatibility["n-samples"][mpe.TASK_NAME] == { - "original": 3, - "effective": 3, + "original": 1, + "effective": 1, } assert "secret" not in json.dumps([native, compatibility]) @@ -221,12 +217,12 @@ def post(payload: dict[str, Any]) -> dict[str, Any]: output_dir = tmp_path / "output" assert not _run(output_dir, post) native = _native(output_dir) - tool_result = native["results"][1] + tool_result = native["results"][0] assert tool_result["tool_calls_valid"] is False assert tool_result["failures"] == ["tool_call_schema"] assert native["metrics"]["ToolCalls-Schema-Accuracy"] == 0.0 - assert native["summary"]["passed_count"] == 2 - assert _score(output_dir) == pytest.approx(2 / 3) + assert native["summary"]["passed_count"] == 0 + assert _score(output_dir) == 0.0 def test_trigger_failure_uses_expected_label(tmp_path: Path) -> None: @@ -240,63 +236,46 @@ def post(payload: dict[str, Any]) -> dict[str, Any]: output_dir = tmp_path / "output" assert not _run(output_dir, post) native = _native(output_dir) - assert native["results"][1]["failures"] == ["tool_call_trigger"] + assert native["results"][0]["failures"] == ["tool_call_trigger"] assert native["metrics"]["ToolCalls-Trigger-Similarity"] == 0.0 assert native["summary"]["tool_calls_finish_stop"] == 1 - assert native["summary"]["stop_finish_stop"] == 1 - - -def test_negative_trigger_requires_stop_finish_reason(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - tools = payload.get("tools", []) - if tools and tools[0]["function"]["name"] == "example": - response = _scenario_response() - response["choices"][0]["finish_reason"] = "length" - return response - return _response_for(payload) - - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - assert native["results"][2]["failures"] == ["tool_call_trigger"] assert native["summary"]["stop_finish_stop"] == 0 - assert native["metrics"]["ToolCalls-Trigger-Similarity"] == 1.0 - -def test_language_failure_uses_pinned_cyrillic_range(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - if not payload.get("tools"): - return _language_response("Это ответ") - return _response_for(payload) - - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - assert native["results"][0]["language_following_checked"] is True - assert native["results"][0]["language_following_valid"] is False - assert native["results"][0]["failures"] == ["language_following"] - assert native["metrics"]["Language-Following-Success-Rate"] == 0.0 +def test_language_validator_uses_pinned_cyrillic_range() -> None: + result = mpe.validate_language("success", "Это ответ") + assert result["language_following_checked"] is True + assert result["language_following_valid"] is False -def test_scenario_failure_uses_visible_first_occurrence_order(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - tools = payload.get("tools", []) - if tools and tools[0]["function"]["name"] == "example": - return _scenario_response( - "123 some-parameter xyz then some-parameter" - ) - return _response_for(payload) - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - result = _native(output_dir)["results"][2] +def test_scenario_validator_uses_visible_first_occurrence_order() -> None: + request = { + "tools": [ + { + "function": { + "parameters": { + "properties": { + "123": {}, + "some-parameter": {}, + "xyz": {}, + "another-parameter": {}, + } + } + } + } + ] + } + result = mpe.validate_scenario( + request, + "success", + "123 some-parameter xyz then some-parameter", + ) assert result["scenario_check_checked"] is True assert result["scenario_check_detail"] == { "expected": ["123", "some-parameter", "xyz", "another-parameter"], "actual": ["xyz", "some-parameter"], } assert result["scenario_check_valid"] is False - assert result["failures"] == ["scenario_check"] def test_transport_retries_once_then_preserves_success(tmp_path: Path) -> None: @@ -317,7 +296,7 @@ def http_post(**request: Any) -> dict[str, Any]: output_dir=output_dir, http_post=http_post, ) - assert attempts == 4 + assert attempts == 2 assert _native(output_dir)["results"][0]["attempts"] == 2 assert _score(output_dir) == 1.0 @@ -487,15 +466,15 @@ def post(payload: dict[str, Any]) -> Any: output_dir = tmp_path / "output" assert not _run(output_dir, post) native = _native(output_dir) - assert calls == 3 - assert len(native["results"]) == 3 + assert calls == 1 + assert len(native["results"]) == 1 for result in native["results"]: assert result["status"] == "failed" assert result["failures"][0] == "query_failed" assert result["response"]["error"]["type"] in {"TypeError", "ValueError"} -def test_exhausted_transport_failure_does_not_skip_later_cases(tmp_path: Path) -> None: +def test_exhausted_transport_failure_publishes_report(tmp_path: Path) -> None: calls = 0 def http_post(**request: Any) -> dict[str, Any]: @@ -514,18 +493,16 @@ def http_post(**request: Any) -> dict[str, Any]: http_post=http_post, ) native = _native(output_dir) - assert calls == 4 - assert len(native["results"]) == 3 + assert calls == 2 + assert len(native["results"]) == 1 assert native["results"][0]["failures"] == [ "query_failed", - "language_following", + "tool_call_trigger", ] - assert native["results"][1]["case_passed"] is True - assert native["results"][2]["case_passed"] is True - assert native["metrics"]["Query-Success-Rate"] == pytest.approx(2 / 3) + assert native["metrics"]["Query-Success-Rate"] == 0.0 -def test_global_deadline_caps_attempts_and_publishes_partial_report( +def test_global_deadline_caps_attempts_and_publishes_timeout_report( tmp_path: Path, ) -> None: now = [0.0] @@ -536,7 +513,7 @@ def clock() -> float: def http_post(**request: Any) -> dict[str, Any]: timeouts.append(request["timeout_seconds"]) - now[0] += 0.7 + now[0] += 1.1 return _response_for(request["payload"]) output_dir = tmp_path / "output" @@ -551,16 +528,14 @@ def http_post(**request: Any) -> dict[str, Any]: clock=clock, ) native = _native(output_dir) - assert timeouts == pytest.approx([1.0, 0.3]) - assert len(native["results"]) == 3 - assert native["results"][0]["case_passed"] is True - assert native["results"][1]["suite_timed_out"] is True - assert native["results"][2]["suite_timed_out"] is True + assert timeouts == pytest.approx([1.0]) + assert len(native["results"]) == 1 + assert native["results"][0]["suite_timed_out"] is True assert native["completed"] is False assert native["integration_error"]["type"] == "SuiteTimeoutError" compatibility = _compatibility(output_dir) assert _score(output_dir) == 0.0 - assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 1 + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 0 assert compatibility["integration_error"]["type"] == "SuiteTimeoutError" @@ -620,20 +595,18 @@ def read1(self, size: int) -> bytes: def test_reasoning_only_response_is_always_checked(tmp_path: Path) -> None: def post(payload: dict[str, Any]) -> dict[str, Any]: - if not payload.get("tools"): - return { - "choices": [ - { - "finish_reason": "length", - "message": { - "reasoning": "I could not answer", - "content": "", - "tool_calls": [], - }, - } - ] - } - return _response_for(payload) + return { + "choices": [ + { + "finish_reason": "length", + "message": { + "reasoning": "I could not answer", + "content": "", + "tool_calls": [], + }, + } + ] + } output_dir = tmp_path / "output" assert not _run(output_dir, post) @@ -641,9 +614,9 @@ def post(payload: dict[str, Any]) -> dict[str, Any]: assert native["results"][0]["error_only_reasoning"] is True assert native["results"][0]["failures"] == [ "error_only_reasoning", - "language_following", + "tool_call_trigger", ] - assert native["metrics"]["Error-Only-Reasoning-Rate"] == pytest.approx(1 / 3) + assert native["metrics"]["Error-Only-Reasoning-Rate"] == 1.0 def test_stale_artifacts_are_removed_without_touching_foreign_results( @@ -691,7 +664,7 @@ def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any: native = _native(output_dir) assert native["completed"] is False - assert len(native["results"]) == 3 + assert len(native["results"]) == 1 assert native["integration_error"] == { "type": "RuntimeError", "message": "dependency installation failed", @@ -699,7 +672,7 @@ def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any: compatibility = _compatibility(output_dir) assert _score(output_dir) == 0.0 assert compatibility["n-samples"][mpe.TASK_NAME] == { - "original": 3, + "original": 1, "effective": 0, } assert compatibility["integration_error"]["message"] == ( @@ -738,17 +711,17 @@ def test_fixture_rejects_unpinned_or_incomplete_input( ) -> None: fixture = json.loads(mpe.DEFAULT_FIXTURE_PATH.read_text(encoding="utf-8")) if field == "indices": - fixture[field] = [0, 71] + fixture[field] = [0] elif field == "ref": fixture[field] = "main" elif field == "license": fixture[field] = "MIT License" elif field == "rows": - fixture[field] = fixture[field][:-1] + fixture[field] = [] elif field == "prompt": fixture["rows"][0]["messages"][0]["content"] = "changed" else: - fixture["rows"][1]["tools"][0]["function"]["parameters"]["type"] = "array" + fixture["rows"][0]["tools"][0]["function"]["parameters"]["type"] = "array" path = tmp_path / "fixture.json" path.write_text(json.dumps(fixture), encoding="utf-8") diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 5a4b504f54..640a2cbcfb 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -384,15 +384,22 @@ def test_minimax_vendor_ignores_single_launcher_concurrency_value() -> None: assert "DISPATCH_COUNT=1" in output -def test_minimax_vendor_rejects_non_m3_model() -> None: - result = _run_invalid_call( - "MODEL=moonshotai/Kimi-K2 " - "MODEL_PREFIX=kimik3 " - "run_minimax_vendor_eval" +def test_minimax_vendor_accepts_non_m3_model() -> None: + script = r''' +source "$BENCHMARK_LIB" +_run_minimax_m3_smoke_eval() { echo "DISPATCH=$EVAL_SUITE"; } +unset EVAL_SUITE EVAL_RESULT_DIR +MODEL=moonshotai/Kimi-K3 MODEL_PREFIX=kimik3 run_minimax_vendor_eval +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=True, ) - assert result.returncode == 2 - assert "requires MODEL_PREFIX=minimaxm3" in result.stderr + assert "DISPATCH=minimax_m3_smoke" in result.stdout def test_minimax_vendor_accepts_case_insensitive_m3_model_name() -> None: From f6cb9092ef5159e4d2d8e0d3dce8e14cfa8a55cb Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:45:53 -0500 Subject: [PATCH 38/99] fix: remove obsolete gb200 recipe field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:移除 NVIDIA srt-slurm v1.0.45 不再支持的 GB200 配置字段。 --- .../deepseek-v4/agentic/disagg-gb200-2p1d-dep8-dep8-agentic.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/disagg-gb200-2p1d-dep8-dep8-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/disagg-gb200-2p1d-dep8-dep8-agentic.yaml index b647276179..4ec6ba81d6 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/disagg-gb200-2p1d-dep8-dep8-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/disagg-gb200-2p1d-dep8-dep8-agentic.yaml @@ -148,7 +148,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: /infmax-workspace From 439d884ad1ae147b0c38d52bef11c67207c7e28c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:44:10 -0500 Subject: [PATCH 39/99] fix: enable frontend tool call parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure the Dynamo frontend to use each SGLang model parser so unary OpenAI responses expose structured tool_calls instead of raw model tags. 中文:配置 Dynamo 前端使用对应的 SGLang 模型解析器,使非流式 OpenAI 响应输出结构化 tool_calls,而不是原始模型标签。 --- .../deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml | 6 ++++++ .../glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml index 1b6101504a..5e3273f5bb 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml @@ -41,6 +41,12 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: + # Dynamo's default Rust chat processor does not consume SGLang's + # deepseekv4 parser setting. Parse DSML tool calls at the frontend so + # non-stream OpenAI responses expose message.tool_calls. + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml index 8b02f5e857..10bf63047b 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml @@ -39,6 +39,11 @@ frontend: DYN_ROUTER_TEMPERATURE: "10000000" PIP_BREAK_SYSTEM_PACKAGES: "1" args: + # Parse GLM tool tags at the Dynamo frontend so non-stream OpenAI + # responses expose message.tool_calls instead of raw tagged content. + dyn-chat-processor: sglang + tool-call-parser: glm47 + reasoning-parser: glm45 router-mode: "kv" router-reset-states: true active-decode-blocks-threshold: "None" From 4fed421a78c343b51e2f92c52ebdc5f341a2b92e Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:50:42 -0500 Subject: [PATCH 40/99] fix: parse GB200 vendor tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use vLLM chat processors at Dynamo frontends for DeepSeek V4 and Kimi K3 so unary OpenAI responses expose structured tool calls. 中文:在 DeepSeek V4 和 Kimi K3 的 Dynamo 前端启用 vLLM 聊天处理器,使非流式 OpenAI 响应输出结构化工具调用。 --- .../deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml | 6 ++++++ .../kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml index f35d7c6bc8..6dd439f98a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml @@ -44,6 +44,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: + # Parse DeepSeek V4 tool tags at the Dynamo frontend so non-streaming + # OpenAI responses expose message.tool_calls instead of raw tagged content. + dyn-chat-processor: vllm + tool-call-parser: deepseek_v4 + reasoning-parser: deepseek_v4 + enable-auto-tool-choice: true router-mode: kv router-reset-states: true router-session-affinity-ttl-secs: 14400 diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index bb1fc550e5..99d32cc24a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -48,7 +48,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: - dyn-chat-processor: "dynamo" + # Use vLLM's Kimi K3 parsers so unary OpenAI responses expose structured + # reasoning and tool_calls instead of raw XTML control tags. + dyn-chat-processor: "vllm" + tool-call-parser: "kimi_k3" + reasoning-parser: "kimi_k3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-temperature: "0" From 6f627448315a9969cbae089799a2127d870db191 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:54:46 -0500 Subject: [PATCH 41/99] fix: force guided Kimi tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the native Dynamo Kimi K3 parser and force structural tags for tool_choice=auto requests so the model emits valid XTML opening markers. 中文:保留 Dynamo 原生 Kimi K3 解析器,并对 tool_choice=auto 请求强制启用结构化标签,使模型输出有效的 XTML 起始标记。 --- .../agentic/agg-gb200-tp16-latency-agentic.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 99d32cc24a..1e52572461 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -48,12 +48,7 @@ frontend: type: dynamo enable_multiple_frontends: false args: - # Use vLLM's Kimi K3 parsers so unary OpenAI responses expose structured - # reasoning and tool_calls instead of raw XTML control tags. - dyn-chat-processor: "vllm" - tool-call-parser: "kimi_k3" - reasoning-parser: "kimi_k3" - enable-auto-tool-choice: true + dyn-chat-processor: "dynamo" router-mode: "kv" router-kv-events: true router-temperature: "0" @@ -111,6 +106,9 @@ backend: scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" dyn-enable-structural-tag: true + # This verifier uses OpenAI's default tool_choice=auto. Force guided + # structural tags so the Kimi K3 parser always receives valid XTML. + dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true From fbb8fe288e3572258f01331ab753500dcdcad421 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:18:07 -0500 Subject: [PATCH 42/99] fix: guide all GB200 Kimi recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the structural-tag scope required for tool_choice=auto to every GB200 Kimi K3 topology, not only the latency recipe used by the smoke run. 中文:为所有 GB200 Kimi K3 拓扑启用 tool_choice=auto 所需的结构化标签范围,而不只覆盖冒烟验证使用的低延迟配置。 --- .../vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml | 1 + .../agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml | 1 + .../vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index 3ab5688e08..62432ac389 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -117,6 +117,7 @@ backend: scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" dyn-enable-structural-tag: true + dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index d80a3d1e49..6d4fc3bb12 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -120,6 +120,7 @@ backend: scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" dyn-enable-structural-tag: true + dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index 6c73e65705..efc6e9e16f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -109,6 +109,7 @@ backend: scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" dyn-enable-structural-tag: true + dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true From 92c8c4f138baf794d1b626c70505e277429ddd56 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:56:50 -0500 Subject: [PATCH 43/99] fix: parse GB200 Kimi tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:使用 vLLM 的 Kimi K3 前端解析器,将工具调用作为结构化 OpenAI 响应返回。 --- .../agentic/agg-gb200-dep16-throughput-agentic.yaml | 9 ++++++--- ...-dep16-throughput-vllm-simple-offload-agentic.yaml | 9 ++++++--- .../agentic/agg-gb200-tep16-balanced-agentic.yaml | 9 ++++++--- .../agentic/agg-gb200-tp16-latency-agentic.yaml | 11 ++++++----- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index 62432ac389..291edd7cc0 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -48,7 +48,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: - dyn-chat-processor: "dynamo" + # Use vLLM's Kimi K3 parser so OpenAI responses expose structured + # tool_calls instead of raw XTML in message.content. + dyn-chat-processor: "vllm" + tool-call-parser: "kimi_k3" + reasoning-parser: "kimi_k3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-temperature: "0" @@ -116,8 +121,6 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" - dyn-enable-structural-tag: true - dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index 6d4fc3bb12..f9e10576e0 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -49,7 +49,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: - dyn-chat-processor: "dynamo" + # Use vLLM's Kimi K3 parser so OpenAI responses expose structured + # tool_calls instead of raw XTML in message.content. + dyn-chat-processor: "vllm" + tool-call-parser: "kimi_k3" + reasoning-parser: "kimi_k3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-temperature: "0" @@ -119,8 +124,6 @@ backend: kv-transfer-config: '{"kv_connector":"SimpleCPUOffloadConnector","kv_role":"kv_both","kv_connector_extra_config":{"cpu_bytes_to_use":549755813888,"cpu_bytes_to_use_per_rank":137438953472,"lazy_offload":false}}' scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" - dyn-enable-structural-tag: true - dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index efc6e9e16f..28b482c454 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -49,7 +49,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: - dyn-chat-processor: "dynamo" + # Use vLLM's Kimi K3 parser so OpenAI responses expose structured + # tool_calls instead of raw XTML in message.content. + dyn-chat-processor: "vllm" + tool-call-parser: "kimi_k3" + reasoning-parser: "kimi_k3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-temperature: "0" @@ -108,8 +113,6 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" - dyn-enable-structural-tag: true - dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 1e52572461..543788c020 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -48,7 +48,12 @@ frontend: type: dynamo enable_multiple_frontends: false args: - dyn-chat-processor: "dynamo" + # Use vLLM's Kimi K3 parser so OpenAI responses expose structured + # tool_calls instead of raw XTML in message.content. + dyn-chat-processor: "vllm" + tool-call-parser: "kimi_k3" + reasoning-parser: "kimi_k3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-temperature: "0" @@ -105,10 +110,6 @@ backend: enable-prefix-caching: true scheduler-cls: "vllm.v1.core.sched.async_scheduler.AsyncScheduler" dyn-tool-call-parser: "kimi_k3" - dyn-enable-structural-tag: true - # This verifier uses OpenAI's default tool_choice=auto. Force guided - # structural tags so the Kimi K3 parser always receives valid XTML. - dyn-structural-tag-scope: "always" reasoning-parser: "kimi_k3" dyn-reasoning-parser: "kimi_k3" no-enable-flashinfer-autotune: true From 7edb59ba5393a856dbfb602f15fbb0c1de0ea57c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:30:14 -0500 Subject: [PATCH 44/99] fix: harden vendor artifact cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:清理多节点供应商评估产物时使用安全的相对 glob,避免文件名被解析为选项。 --- .github/workflows/benchmark-multinode-tmpl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 6eff3dafe0..6ff9d95e73 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -514,7 +514,7 @@ jobs: run: | rm -f meta_env.json || true rm -f results*.json || true - rm -f *_vendor_report.json || true + rm -f -- ./*_vendor_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true From ade0abd62ce1db378bb44755f7398ef9b5a00975 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 00:08:50 -0500 Subject: [PATCH 45/99] fix: harden transient endpoint registration retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:对服务端点注册期间的瞬时 404 和传输错误执行有界退避重试,避免 Dynamo 前端刚启动时的误报。 --- utils/evals/minimax_provider_eval.py | 18 +++++++++++++----- utils/evals/test_minimax_provider_eval.py | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index ecd937a2b9..3120b28dd1 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -41,14 +41,16 @@ 71: "10272004ae08f4a7d08d2306404f6cbb7bbfa794230e1082a235ded036d550ed", } MAX_RESPONSE_BYTES = 16 * 1024 * 1024 -MAX_ATTEMPTS = 2 +RETRY_BACKOFF_SECONDS = (5.0, 10.0, 20.0) +MAX_ATTEMPTS = len(RETRY_BACKOFF_SECONDS) + 1 HttpPost = Callable[..., Any] Clock = Callable[[], float] +Sleeper = Callable[[float], None] class TransportError(OSError): - """An HTTP transport failure that may be retried once.""" + """An HTTP transport failure that may be retried with bounded backoff.""" class SuiteTimeoutError(TimeoutError): @@ -242,7 +244,7 @@ def _default_http_post( with _NO_REDIRECT_OPENER.open(request, timeout=timeout_seconds) as response: content = _read_response_body(response, deadline).decode("utf-8") except urllib.error.HTTPError as exc: - if exc.code == 429 or 500 <= exc.code < 600: + if exc.code in {404, 429} or 500 <= exc.code < 600: raise TransportError(f"HTTP {exc.code}: {exc.reason}") from exc raise ValueError( f"chat completion request failed with HTTP {exc.code}: {exc.reason}" @@ -553,6 +555,7 @@ def _evaluate_case( deadline: float, http_post: HttpPost, clock: Clock, + sleeper: Sleeper, ) -> dict[str, Any]: prepared = prepare_request(row, model) started = clock() @@ -597,6 +600,9 @@ def _evaluate_case( break request_error = exc if attempt + 1 < MAX_ATTEMPTS: + delay = min(RETRY_BACKOFF_SECONDS[attempt], deadline - clock()) + if delay > 0: + sleeper(delay) continue break except Exception as exc: # noqa: BLE001 - preserve per-request diagnostics @@ -930,6 +936,7 @@ def run_evaluation( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, http_post: HttpPost = _default_http_post, clock: Clock = time.monotonic, + sleeper: Sleeper = time.sleep, ) -> bool: """Run the pinned case and always publish both artifacts.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -942,8 +949,8 @@ def run_evaluation( request_timeout_seconds, "request_timeout_seconds" ) suite_timeout = _positive_number(timeout_seconds, "timeout_seconds") - if not callable(http_post) or not callable(clock): - raise TypeError("http_post and clock must be callable") + if not callable(http_post) or not callable(clock) or not callable(sleeper): + raise TypeError("http_post, clock, and sleeper must be callable") deadline = clock() + suite_timeout if not isinstance(model, str) or not model.strip(): raise ValueError("model must be a non-empty string") @@ -981,6 +988,7 @@ def run_evaluation( deadline=deadline, http_post=http_post, clock=clock, + sleeper=sleeper, ) except Exception as exc: # noqa: BLE001 - continue and report every case result = _failed_case_result(row, exc) diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 61893773b2..bfba0ed08c 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -278,8 +278,11 @@ def test_scenario_validator_uses_visible_first_occurrence_order() -> None: assert result["scenario_check_valid"] is False -def test_transport_retries_once_then_preserves_success(tmp_path: Path) -> None: +def test_transport_retries_with_backoff_then_preserves_success( + tmp_path: Path, +) -> None: attempts = 0 + sleeps: list[float] = [] def http_post(**request: Any) -> dict[str, Any]: nonlocal attempts @@ -295,8 +298,10 @@ def http_post(**request: Any) -> dict[str, Any]: model="MiniMax-M3", output_dir=output_dir, http_post=http_post, + sleeper=sleeps.append, ) assert attempts == 2 + assert sleeps == [5.0] assert _native(output_dir)["results"][0]["attempts"] == 2 assert _score(output_dir) == 1.0 @@ -305,6 +310,7 @@ def http_post(**request: Any) -> dict[str, Any]: ("status_code", "error_type"), ( (400, ValueError), + (404, mpe.TransportError), (408, ValueError), (429, mpe.TransportError), (503, mpe.TransportError), @@ -476,13 +482,12 @@ def post(payload: dict[str, Any]) -> Any: def test_exhausted_transport_failure_publishes_report(tmp_path: Path) -> None: calls = 0 + sleeps: list[float] = [] def http_post(**request: Any) -> dict[str, Any]: nonlocal calls calls += 1 - if calls <= 2: - raise mpe.TransportError("offline") - return _response_for(request["payload"]) + raise mpe.TransportError("offline") output_dir = tmp_path / "output" assert not mpe.run_evaluation( @@ -491,9 +496,11 @@ def http_post(**request: Any) -> dict[str, Any]: model="MiniMax-M3", output_dir=output_dir, http_post=http_post, + sleeper=sleeps.append, ) native = _native(output_dir) - assert calls == 2 + assert calls == 4 + assert sleeps == [5.0, 10.0, 20.0] assert len(native["results"]) == 1 assert native["results"][0]["failures"] == [ "query_failed", From 25675699a079535070fc4cc3903e6a2957e0a7d3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:25:24 -0500 Subject: [PATCH 46/99] fix: allow trusted Kimi frontend code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:允许 Kimi 前端加载可信代码。 --- .../vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml | 1 + .../agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml | 1 + .../vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml | 1 + .../vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index 291edd7cc0..87efbb386c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -51,6 +51,7 @@ frontend: # Use vLLM's Kimi K3 parser so OpenAI responses expose structured # tool_calls instead of raw XTML in message.content. dyn-chat-processor: "vllm" + trust-remote-code: true tool-call-parser: "kimi_k3" reasoning-parser: "kimi_k3" enable-auto-tool-choice: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index f9e10576e0..df90355c79 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -52,6 +52,7 @@ frontend: # Use vLLM's Kimi K3 parser so OpenAI responses expose structured # tool_calls instead of raw XTML in message.content. dyn-chat-processor: "vllm" + trust-remote-code: true tool-call-parser: "kimi_k3" reasoning-parser: "kimi_k3" enable-auto-tool-choice: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index 28b482c454..806f571bea 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -52,6 +52,7 @@ frontend: # Use vLLM's Kimi K3 parser so OpenAI responses expose structured # tool_calls instead of raw XTML in message.content. dyn-chat-processor: "vllm" + trust-remote-code: true tool-call-parser: "kimi_k3" reasoning-parser: "kimi_k3" enable-auto-tool-choice: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 543788c020..4bd2fb36d9 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -51,6 +51,7 @@ frontend: # Use vLLM's Kimi K3 parser so OpenAI responses expose structured # tool_calls instead of raw XTML in message.content. dyn-chat-processor: "vllm" + trust-remote-code: true tool-call-parser: "kimi_k3" reasoning-parser: "kimi_k3" enable-auto-tool-choice: true From a84747927e2017435be3aaa2b515aee6e4d06a7f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 03:40:32 -0500 Subject: [PATCH 47/99] feat: add deterministic bfcl tool smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin and verify BFCL 2026.3.23, run four exact V4 cases against OpenAI-compatible chat completions, preserve native diagnostics, and gate the aggregate score at 0.75. 中文:新增确定性的 BFCL 工具调用冒烟评估。固定并校验 BFCL 2026.3.23,针对 OpenAI 兼容的 chat completions 端点运行四个精确的 V4 用例,保留原生诊断产物,并以 0.75 阈值校验汇总分数。 --- .../workflows/benchmark-multinode-tmpl.yml | 12 +- .github/workflows/benchmark-tmpl.yml | 13 +- .github/workflows/e2e-tests.yml | 8 +- benchmarks/benchmark_lib.sh | 263 ++++++- utils/collect_eval_results.py | 2 + utils/evals/EVALS.md | 108 ++- utils/evals/bfcl_eval.py | 711 ++++++++++++++++++ utils/evals/test_batched_eval.py | 5 + utils/evals/test_bfcl_eval.py | 536 +++++++++++++ utils/evals/test_run_eval_dispatch.py | 379 +++++++++- utils/evals/thresholds.yaml | 6 + utils/test_collect_eval_results.py | 56 +- 12 files changed, 2058 insertions(+), 41 deletions(-) create mode 100755 utils/evals/bfcl_eval.py create mode 100644 utils/evals/test_bfcl_eval.py diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 6ff9d95e73..68d0bee0d1 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -139,12 +139,12 @@ on: required: false default: false eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" type: string required: false default: "lm-eval" eval-suite: - description: "Provider verifier suite; leave empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" type: string required: false default: "" @@ -497,6 +497,7 @@ jobs: agent_preds.json predictions.jsonl swebench_report_*.json + bfcl_report.json *.traj* if-no-files-found: ${{ inputs.eval-only && 'error' || 'ignore' }} @@ -507,7 +508,11 @@ jobs: if [[ -z "${expected_concs}" ]]; then expected_concs="$(printf '%s\n' "${CONC_LIST}" | tr ' ' '\n' | sort -n | tail -1)" fi - python3 utils/evals/validate_scores.py --expected-concs "${expected_concs}" + if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then + python3 utils/evals/validate_scores.py --metric-prefix 'acc,' --expected-concs "${expected_concs}" + else + python3 utils/evals/validate_scores.py --expected-concs "${expected_concs}" + fi - name: Cleanup eval outputs (post-upload) if: ${{ always() && (inputs.run-eval || inputs.eval-only) }} @@ -515,6 +520,7 @@ jobs: rm -f meta_env.json || true rm -f results*.json || true rm -f -- ./*_vendor_report.json || true + rm -f bfcl_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 7aec3622e5..a3ed000923 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -91,12 +91,12 @@ on: required: false default: false eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" type: string required: false default: "lm-eval" eval-suite: - description: "Provider verifier suite; leave empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" type: string required: false default: "" @@ -414,6 +414,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + bfcl_report.json sample*.jsonl agent_preds.json predictions.jsonl @@ -423,7 +424,12 @@ jobs: - name: Verify eval scores if: ${{ (success() || failure()) && inputs.eval-only }} - run: python3 utils/evals/validate_scores.py + run: | + if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then + python3 utils/evals/validate_scores.py --metric-prefix 'acc,' + else + python3 utils/evals/validate_scores.py + fi - name: Cleanup eval outputs (post-upload) if: ${{ always() && (env.RUN_EVAL == 'true' || inputs.eval-only) }} @@ -433,6 +439,7 @@ jobs: rm -f results*.json || true rm -f -- ./*_vendor_report.json || true rm -f sample*.jsonl || true + rm -f bfcl_report.json || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true - name: Resource cleanup (post-run) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ee49969ae1..052fc69c0b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,12 +46,12 @@ on: type: string default: "" eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" required: false type: string default: "lm-eval" eval-suite: - description: "Provider verifier suite; empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" required: false type: string default: "" @@ -136,12 +136,12 @@ on: type: string default: "" eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, or minimax-vendor)" + description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" required: false type: string default: "lm-eval" eval-suite: - description: "Provider verifier suite; empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" required: false type: string default: "" diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index c27a76085b..4087378d01 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -895,13 +895,21 @@ _install_lm_eval_deps() { _prepare_vendor_verifier_python() { local verifier_name="$1" local runtime_prefix="$2" + local use_system_site_packages="${3:-false}" + local minimum_python_minor="${4:-12}" VENDOR_VERIFIER_PYTHON=python3 VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="" export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR - if python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))'; then - return 0 + local system_python_is_compatible=false + if python3 -c \ + 'import sys; raise SystemExit(sys.version_info < (3, int(sys.argv[1])))' \ + "$minimum_python_minor"; then + system_python_is_compatible=true + if [ "$use_system_site_packages" != "true" ]; then + return 0 + fi fi local python_dir uv_prefix uv_bin venv_dir prepare_rc=0 @@ -912,23 +920,32 @@ _prepare_vendor_verifier_python() { VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$python_dir" export VENDOR_VERIFIER_PYTHON_CLEANUP_DIR - uv_prefix="${python_dir}/uv" - uv_bin="${uv_prefix}/bin/uv" venv_dir="${python_dir}/venv" - python3 -m pip install -q --no-cache-dir --break-system-packages \ - --prefix "$uv_prefix" "uv==0.11.33" || prepare_rc=$? - if [ "$prepare_rc" -eq 0 ] && [ ! -x "$uv_bin" ]; then - echo "ERROR: pinned uv installation did not create ${uv_bin}" >&2 - prepare_rc=1 - fi - if [ "$prepare_rc" -eq 0 ]; then - UV_CACHE_DIR="${python_dir}/uv-cache" \ - UV_PYTHON_INSTALL_DIR="${python_dir}/python" \ - "$uv_bin" venv --python 3.12 --seed "$venv_dir" \ - || prepare_rc=$? + if [ "$system_python_is_compatible" = "true" ]; then + python3 -m venv --system-site-packages "$venv_dir" || prepare_rc=$? + else + uv_prefix="${python_dir}/uv" + uv_bin="${uv_prefix}/bin/uv" + python3 -m pip install -q --no-cache-dir --break-system-packages \ + --prefix "$uv_prefix" "uv==0.11.33" || prepare_rc=$? + if [ "$prepare_rc" -eq 0 ] && [ ! -x "$uv_bin" ]; then + echo "ERROR: pinned uv installation did not create ${uv_bin}" >&2 + prepare_rc=1 + fi + if [ "$prepare_rc" -eq 0 ]; then + local system_site_packages_args=() + if [ "$use_system_site_packages" = "true" ]; then + system_site_packages_args+=(--system-site-packages) + fi + UV_CACHE_DIR="${python_dir}/uv-cache" \ + UV_PYTHON_INSTALL_DIR="${python_dir}/python" \ + "$uv_bin" venv --python "3.${minimum_python_minor}" --seed \ + "${system_site_packages_args[@]}" "$venv_dir" \ + || prepare_rc=$? + fi fi if [ "$prepare_rc" -eq 0 ] && [ ! -x "${venv_dir}/bin/python" ]; then - echo "ERROR: pinned uv did not create the ${verifier_name} Python interpreter" >&2 + echo "ERROR: pinned Python setup did not create the ${verifier_name} interpreter" >&2 prepare_rc=1 fi if [ "$prepare_rc" -ne 0 ]; then @@ -1324,6 +1341,201 @@ run_kimi_vendor_eval() { esac } +_install_bfcl_eval_deps() { + local download_dir="$1" + local wheel_url="https://files.pythonhosted.org/packages/ba/41/ed458527c770c50225b60bae3b0c3444b26804ee455fa2d8f187018d2cb2/bfcl_eval-2026.3.23-py3-none-any.whl" + local wheel_sha256="3bb6dfa5f0c68ad403c9ec50b00db2bb3b4cc9b38ab1ff33f48fe30d853d3a0a" + local wheel_path="${download_dir}/bfcl_eval-2026.3.23-py3-none-any.whl" + + "${VENDOR_VERIFIER_PYTHON:-python3}" - \ + "$wheel_url" "$wheel_sha256" "$wheel_path" <<'PY' || return $? +from hashlib import sha256 +from pathlib import Path +import sys +from urllib.request import Request, urlopen + +wheel_url, expected_sha256, wheel_path_arg = sys.argv[1:] +wheel_path = Path(wheel_path_arg) +digest = sha256() +downloaded = 0 +request = Request( + wheel_url, + headers={"User-Agent": "InferenceX-BFCL-Smoke"}, +) + +try: + with urlopen(request, timeout=180) as response, wheel_path.open("xb") as output: + while chunk := response.read(1024 * 1024): + downloaded += len(chunk) + if downloaded > 512 * 1024 * 1024: + raise ValueError("BFCL wheel exceeds the 512 MiB safety limit") + digest.update(chunk) + output.write(chunk) + if downloaded == 0: + raise ValueError("downloaded BFCL wheel is empty") + actual_sha256 = digest.hexdigest() + if actual_sha256 != expected_sha256: + raise ValueError( + f"BFCL wheel SHA256 mismatch: expected {expected_sha256}, " + f"got {actual_sha256}" + ) +except Exception as error: + wheel_path.unlink(missing_ok=True) + print(f"ERROR: failed to download and verify the pinned BFCL wheel: {error}", file=sys.stderr) + raise SystemExit(1) +PY + + timeout 600 "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install \ + -q --no-cache-dir "$wheel_path" +} + +_prepare_bfcl_runtime() { + local runtime_dir install_rc=0 + runtime_dir="$(mktemp -d /tmp/bfcl-runtime-XXXXXX)" || return $? + _install_bfcl_eval_deps "$runtime_dir" >&2 || install_rc=$? + if [ "$install_rc" -ne 0 ]; then + rm -rf "$runtime_dir" + return "$install_rc" + fi + printf '%s\n' "$runtime_dir" +} + +_write_bfcl_integration_error() { + local adapter_path="$1" + local model_name="$2" + local results_dir="$3" + local message="$4" + local adapter_rc=0 + + # Integration errors deliberately make the adapter exit nonzero after + # publishing both score artifacts. Treat those artifacts, not that expected + # status, as proof that failure reporting succeeded. + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --integration-error "$message" \ + || adapter_rc=$? + if [ -f "${results_dir}/bfcl_report.json" ] \ + && [ -f "${results_dir}/results_bfcl.json" ]; then + return 0 + fi + if [ "$adapter_rc" -eq 0 ]; then + return 1 + fi + return "$adapter_rc" +} + +_run_bfcl_smoke_eval() { + local port="${PORT:-8888}" + local results_dir="${EVAL_RESULT_DIR:-}" + + while [[ $# -gt 0 ]]; do + case "$1" in + --port|--results-dir) + if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then + echo "ERROR: $1 requires a value" >&2 + return 2 + fi + case "$1" in + --port) port="$2" ;; + --results-dir) results_dir="$2" ;; + esac + shift 2 + ;; + *) + echo "Unknown parameter: $1" >&2 + return 2 + ;; + esac + done + + if [ -z "$results_dir" ]; then + results_dir="$(mktemp -d /tmp/eval_out-XXXXXX)" || return $? + fi + + local model_name="${MODEL_NAME:-${MODEL:-}}" + local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/bfcl_eval.py" + local runtime_dir="" + local project_root="" + + mkdir -p "$results_dir" || return $? + results_dir="$(cd "$results_dir" && pwd)" || return $? + export EVAL_RESULT_DIR="$results_dir" + + local setup_rc=0 integration_error="" + _prepare_vendor_verifier_python "BFCL" "bfcl-python" true 10 || { + setup_rc=$? + integration_error="BFCL Python runtime preparation failed with exit code ${setup_rc}" + } + if [ "$setup_rc" -eq 0 ]; then + runtime_dir=$(_prepare_bfcl_runtime) || { + setup_rc=$? + integration_error="BFCL dependency installation failed with exit code ${setup_rc}" + } + fi + if [ "$setup_rc" -eq 0 ]; then + project_root="$(mktemp -d /tmp/bfcl-project-root-XXXXXX)" || { + setup_rc=$? + integration_error="BFCL project root preparation failed with exit code ${setup_rc}" + } + fi + if [ "$setup_rc" -ne 0 ]; then + echo "ERROR: ${integration_error}" >&2 + local artifact_rc=0 + _write_bfcl_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write BFCL failure artifact (exit code ${artifact_rc})" >&2 + fi + _cleanup_vendor_eval \ + "$runtime_dir" "$project_root" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + return "$setup_rc" + fi + + local eval_rc=0 + timeout 900 "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --api-key EMPTY \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --bfcl-project-root "$project_root" \ + --num-threads 4 \ + --request-timeout-seconds 180 \ + || eval_rc=$? + if [ "$eval_rc" -ne 0 ] \ + && { [ ! -f "${results_dir}/bfcl_report.json" ] \ + || [ ! -f "${results_dir}/results_bfcl.json" ]; }; then + local integration_error="BFCL evaluation failed with exit code ${eval_rc}" + local artifact_rc=0 + _write_bfcl_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write BFCL failure artifact (exit code ${artifact_rc})" >&2 + fi + fi + _cleanup_vendor_eval \ + "$runtime_dir" "$project_root" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + return "$eval_rc" +} + +run_bfcl_eval() { + local eval_suite="${EVAL_SUITE:-bfcl_smoke}" + export EVAL_SUITE="$eval_suite" + + case "$eval_suite" in + bfcl_smoke) + _run_bfcl_smoke_eval "$@" + ;; + *) + echo "ERROR: unsupported BFCL suite '${eval_suite}'" >&2 + export EVAL_RESULT_DIR="" + return 2 + ;; + esac +} + _install_minimax_vendor_eval_deps() { local target_dir="$1" "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ @@ -2272,6 +2484,9 @@ run_eval() { minimax-vendor) [ -n "${EVAL_SUITE:-}" ] || EVAL_SUITE="minimax_m3_smoke" ;; + bfcl) + [ -n "${EVAL_SUITE:-}" ] || EVAL_SUITE="bfcl_smoke" + ;; esac case "${EVAL_SUITE:-}" in @@ -2284,15 +2499,17 @@ run_eval() { if [ -n "${EVAL_SUITE:-}" ] \ && [ "$framework" != "kimi-vendor" ] \ - && [ "$framework" != "minimax-vendor" ]; then - echo "ERROR: EVAL_SUITE is only supported with a provider verifier framework (kimi-vendor or minimax-vendor)" >&2 + && [ "$framework" != "minimax-vendor" ] \ + && [ "$framework" != "bfcl" ]; then + echo "ERROR: EVAL_SUITE is only supported with kimi-vendor, minimax-vendor, or bfcl" >&2 return 2 fi - # Provider verifier suites use fixed request budgets and do not consume + # Explicit verifier suites use fixed request budgets and do not consume # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for those paths. if [ "$framework" != "kimi-vendor" ] \ && [ "$framework" != "minimax-vendor" ] \ + && [ "$framework" != "bfcl" ] \ && [ -z "${EVAL_MAX_MODEL_LEN:-}" ]; then compute_eval_context_length "$MODEL" "${MAX_MODEL_LEN:-0}" > /dev/null fi @@ -2366,6 +2583,7 @@ run_eval() { swebench) run_swebench_eval "${forwarded[@]}" || eval_rc=$? ;; kimi-vendor) run_kimi_vendor_eval "${forwarded[@]}" || eval_rc=$? ;; minimax-vendor) run_minimax_vendor_eval "${forwarded[@]}" || eval_rc=$? ;; + bfcl) run_bfcl_eval "${forwarded[@]}" || eval_rc=$? ;; *) echo "Unknown framework '${framework}'"; eval_rc=1 ;; esac @@ -2373,11 +2591,12 @@ run_eval() { export EVAL_COMPLETED_SUITE="$EVAL_SUITE" fi - # Agentic eval-only recipes have no separate staging step. Provider - # verifier failures also carry diagnostic score artifacts to preserve. + # Agentic eval-only recipes have no separate staging step. Verifier failures + # also carry diagnostic score artifacts to preserve. if { [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; } \ || { { [ "$framework" = "kimi-vendor" ] \ - || [ "$framework" = "minimax-vendor" ]; } \ + || [ "$framework" = "minimax-vendor" ] \ + || [ "$framework" = "bfcl" ]; } \ && [ "$eval_rc" -ne 0 ]; }; then append_lm_eval_summary || true fi diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 881e6d3e14..dc48d9648b 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -216,6 +216,8 @@ def get_val_se(filter_name: str) -> Tuple[Optional[float], Optional[float]]: # SWE-bench uses resolved rate as its primary score. if 'strict' in fname or 'resolved' in fname: strict_val, strict_se = get_val_se(fname) + elif base_metric == 'acc' and fname == 'none': + accuracy_val, accuracy_se = get_val_se(fname) elif 'flex' in fname or 'extract' in fname: flex_val, flex_se = get_val_se(fname) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 3fe5a31f53..0d9ae33f75 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -63,11 +63,11 @@ append_lm_eval_summary python3 utils/evals/validate_scores.py ``` -The framework selects a provider-specific subprocess adapter, while the suite +The framework selects a suite-specific subprocess adapter, while the suite selects a case set understood by that adapter. Each adapter owns its endpoint -format, dependencies, native report, metrics, and pass policy. Kimi and MiniMax -use separate explicit `run_eval` cases; future provider or BFCL support should -do the same rather than introduce a shared request or report abstraction. +format, dependencies, native report, metrics, and integration-failure policy. +Kimi, MiniMax, and BFCL use separate explicit `run_eval` cases rather than a +shared request or report abstraction. Agentic eval jobs forward the matrix `spec-decoding` value, so MTP entries launch their existing `*_mtp.sh` server instead of silently falling back to STP. @@ -186,6 +186,99 @@ pass-at-k behavior, streaming behavior, parallel-call behavior, multi-turn tool execution, language following, scenario key-order recall, or general agent quality. +### BFCL V4 deterministic tool-use smoke + +The BFCL smoke is opt-in for models served through an OpenAI-compatible +chat-completions endpoint. Select `eval-framework: bfcl` and +`eval-suite: bfcl_smoke` in `e2e-tests.yml`, or run it from the repository root +against an already-ready server: + +```bash +source benchmarks/benchmark_lib.sh +export EVAL_FRAMEWORK=bfcl +export MODEL_NAME="" +export EVAL_SUITE=bfcl_smoke +export EVAL_RESULT_DIR="$(mktemp -d /tmp/eval_out-XXXXXX)" +run_eval --port "$PORT" +append_lm_eval_summary +python3 utils/evals/validate_scores.py --metric-prefix 'acc,' +``` + +The runtime pins +[`bfcl-eval==2026.3.23`](https://pypi.org/project/bfcl-eval/2026.3.23/), built +from Gorilla commit +[`6ea57973c7a6097fd7c5915698c54c17c5b1b6c8`](https://github.com/ShishirPatil/gorilla/commit/6ea57973c7a6097fd7c5915698c54c17c5b1b6c8). +It downloads the exact +[`bfcl_eval-2026.3.23-py3-none-any.whl`](https://files.pythonhosted.org/packages/ba/41/ed458527c770c50225b60bae3b0c3444b26804ee455fa2d8f187018d2cb2/bfcl_eval-2026.3.23-py3-none-any.whl) +and verifies SHA256 +`3bb6dfa5f0c68ad403c9ec50b00db2bb3b4cc9b38ab1ff33f48fe30d853d3a0a` +before installation. The integration follows the pinned +[vLLM perf-eval BFCL runner](https://github.com/vllm-project/perf-eval/blob/7ecb11405df86b202f4c5cca322bd133052fee82/lib/run_bfcl.py), +but uses a fixed four-case V4 partial evaluation: + +| BFCL category | Exact upstream case ID | Projected task | +|---------------|------------------------|----------------| +| `simple_python` | `simple_python_0` | `bfcl_simple_python` | +| `multiple` | `multiple_9` | `bfcl_multiple` | +| `parallel` | `parallel_1` | `bfcl_parallel` | +| `irrelevance` | `irrelevance_0` | `bfcl_irrelevance` | + +The verified wheel is installed into a temporary Python 3.10-or-newer virtual +environment with system site packages enabled so the image's existing +Torch/Transformers stack can be reused; it never mutates the global Python +environment. The temporary environment and BFCL project root are removed after +the run. Once package installation finishes, evaluation is local-only: BFCL +skips its server setup and uses only the already-running local API root, +typically `http://127.0.0.1:$PORT/v1`. The OpenAI SDK appends +`/chat/completions`; the adapter base URL is not the full endpoint. BFCL does +not download a model or call a remote inference API. + +The smoke fixes temperature to `0`, uses four BFCL worker threads, allows 180 +seconds per OpenAI request with retries disabled, and has a 900-second +whole-suite timeout. Dependency installation is separately bounded at 600 +seconds. Dependency, setup, transport, timeout, and collection failures write +zero-score artifacts with integration-error metadata and fail the runner +nonzero. A completed evaluation exits independently of model quality; the +workflow score-validation step applies the threshold afterward. + +The endpoint must implement OpenAI chat completions at `/v1/chat/completions`, +accept `tools` and the tool-selection fields emitted by BFCL, and return the +served model's OpenAI tool-call shape. In particular, assistant tool calls need +function names and JSON-encoded `function.arguments`; the response must also +support a normal no-tool answer for the irrelevance case. Starting a nominally +OpenAI-compatible server is not sufficient if it cannot parse that model's +native tool-call syntax. + +Configure the server's model-specific function-calling parser and, when the +model's default template does not render tools correctly, its tool-aware chat +template. For vLLM, automatic calls require `--enable-auto-tool-choice` plus +`--tool-call-parser`, with `--chat-template` when needed. SGLang uses its +corresponding `--tool-call-parser`; TensorRT-LLM uses `--tool_parser`, plus the +matching reasoning-parser option when the model requires one. Parser names are +engine-specific. Current common mappings are Kimi K3 (`kimi_k3`), MiniMax M3 +(`minimax_m3` in vLLM/TRT-LLM and `minimax-m3` in SGLang), and DeepSeek V4 +(`deepseek_v4` in vLLM/TRT-LLM and `deepseekv4` in SGLang). GLM-4.5 uses +`glm45` in vLLM/SGLang, while GLM-4.7 uses `glm47`; Qwen3-Coder uses +`qwen3_coder` in vLLM/SGLang, with `qwen3_xml` for vLLM's XML variant and +`qwen3` for the corresponding TensorRT-LLM parser. The model recipe and +installed engine version are authoritative; BFCL does not replace a missing or +mismatched parser/chat template. + +`bfcl_report.json` is the native report. `results_bfcl.json` is the +`inferencex-eval-v1` compatibility result consumed by the existing artifact +upload, `append_lm_eval_summary`, collector, and score validator. It projects +the four-case aggregate as task `bfcl_smoke` and the four one-case diagnostic +tasks shown above. Every row uses lm-eval-compatible `acc,none` (plus +`acc_stderr,none`); BFCL workflows therefore validate with metric prefix +`acc,` rather than the default exact-match prefix. + +Only `bfcl_smoke` gates the run: its `0.75` threshold requires at least three +of the four fixed upstream cases to be correct. The four `bfcl_` +thresholds are `0.0`, so their one-case scores remain diagnostic and a single +failed category does not become a second gate. BFCL reuses the existing eval +job, upload paths, aggregation, and validation instead of adding a parallel +workflow or artifact route. + ### Benchmark script flow All benchmark scripts in `benchmarks/` follow one of two flows: @@ -217,11 +310,14 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | | `run_minimax_vendor_eval` | Runs the pinned single-case MiniMax provider smoke | +| `run_bfcl_eval` | Runs the pinned four-case BFCL V4 smoke | | `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_prepare_vendor_verifier_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime for provider verifiers | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | | `_prepare_minimax_vendor_runtime` | Installs the pinned MiniMax adapter dependency in an isolated temp path | +| `_prepare_bfcl_runtime` | Installs the verified BFCL wheel in a temporary virtual environment | +| `_install_bfcl_eval_deps` | Downloads, verifies, and installs the pinned BFCL wheel | | `_prepare_kimi_vendor_verifier` | Downloads and safely extracts a fresh subset of the pinned source archive | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | @@ -334,8 +430,8 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' 2. Add an explicit framework case in `run_eval`; keep suite-specific policy in that adapter's shell runner. 3. Install dependencies in a provider-specific isolated runtime. -4. Emit `result_format: inferencex-eval-v1`, preserve the native report as - `*_vendor_report.json`, set `EVAL_SUITE`, and add a threshold. +4. Emit `result_format: inferencex-eval-v1`, preserve the native report in an + explicitly uploaded suite-specific path, set `EVAL_SUITE`, and add a threshold. ### Runtime patches (`utils/evals/patches/`) diff --git a/utils/evals/bfcl_eval.py b/utils/evals/bfcl_eval.py new file mode 100755 index 0000000000..692e2a7736 --- /dev/null +++ b/utils/evals/bfcl_eval.py @@ -0,0 +1,711 @@ +#!/usr/bin/env python3 +"""Run the pinned four-case BFCL V4 OpenAI chat-completions smoke.""" + +from __future__ import annotations + +import argparse +import inspect +import json +import math +import os +import sys +import time +import urllib.parse +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from queue import SimpleQueue +from typing import Any, Protocol + +TASK_NAME = "bfcl_smoke" +NATIVE_REPORT_FILENAME = "bfcl_report.json" +COMPATIBILITY_FILENAME = "results_bfcl.json" +COMPATIBILITY_GLOB = "results_bfcl*.json" +RESULT_FORMAT = "inferencex-eval-v1" +ADAPTER_NAME = "bfcl-v4-openai-completions" +DEFAULT_NUM_THREADS = 4 +DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 +REQUIRED_SCORE = 0.75 + +BFCL_PACKAGE = "bfcl-eval" +BFCL_PACKAGE_VERSION = "2026.3.23" +BFCL_WHEEL_SHA256 = "3bb6dfa5f0c68ad403c9ec50b00db2bb3b4cc9b38ab1ff33f48fe30d853d3a0a" +UPSTREAM_REPOSITORY = "https://github.com/ShishirPatil/gorilla" +UPSTREAM_SOURCE = "https://pypi.org/project/bfcl-eval/2026.3.23/" +UPSTREAM_REF = f"{BFCL_PACKAGE}=={BFCL_PACKAGE_VERSION}" +SOURCE_REVISION = "6ea57973c7a6097fd7c5915698c54c17c5b1b6c8" +VLLM_INTEGRATION_REF = "7ecb11405df86b202f4c5cca322bd133052fee82" + +# Dict insertion order is intentional: reports and the upstream run-ID file are stable. +SMOKE_CASE_IDS: dict[str, tuple[str, ...]] = { + "simple_python": ("simple_python_0",), + "multiple": ("multiple_9",), + "parallel": ("parallel_1",), + "irrelevance": ("irrelevance_0",), +} +EXPECTED_SAMPLE_COUNT = sum(len(case_ids) for case_ids in SMOKE_CASE_IDS.values()) + + +class UpstreamRunner(Protocol): + """Injectable boundary around the optional BFCL installation.""" + + def __call__( + self, + *, + model: str, + project_root: Path, + base_url: str, + api_key: str, + num_threads: int, + request_timeout_seconds: float, + ) -> None: ... + + +@dataclass(frozen=True) +class CategoryScore: + category: str + case_ids: tuple[str, ...] + score_file: str + header: Mapping[str, Any] + records: tuple[Mapping[str, Any], ...] + accuracy: float + correct_count: int + total_count: int + + def as_dict(self) -> dict[str, Any]: + return { + "category": self.category, + "case_ids": list(self.case_ids), + "score_file": self.score_file, + "score_header": dict(self.header), + "score_records": [dict(record) for record in self.records], + "case_scores": [ + { + "id": case_id, + "score": self.accuracy, + "correct": self.correct_count == self.total_count, + } + for case_id in self.case_ids + ], + } + + +def _nonempty_string(value: str) -> str: + if not value.strip(): + raise argparse.ArgumentTypeError("must be a non-empty string") + return value.strip() + + +def _absolute_http_url(value: str) -> str: + normalized = _nonempty_string(value).rstrip("/") + parsed = urllib.parse.urlsplit(normalized) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise argparse.ArgumentTypeError("must be an absolute HTTP(S) URL") + if parsed.path.rstrip("/").endswith("/chat/completions"): + raise argparse.ArgumentTypeError( + "must be an API root URL; the OpenAI client appends /chat/completions" + ) + return normalized + + +def _positive_int(value: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("must be a positive integer") from exc + if parsed <= 0: + raise argparse.ArgumentTypeError("must be a positive integer") + return parsed + + +def _positive_float(value: str) -> float: + try: + parsed = float(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("must be a positive finite number") from exc + if not math.isfinite(parsed) or parsed <= 0: + raise argparse.ArgumentTypeError("must be a positive finite number") + return parsed + + +def _source_details() -> dict[str, Any]: + return { + "url": UPSTREAM_SOURCE, + "repository": UPSTREAM_REPOSITORY, + "ref": UPSTREAM_REF, + "package": BFCL_PACKAGE, + "package_version": BFCL_PACKAGE_VERSION, + "wheel_sha256": BFCL_WHEEL_SHA256, + "source_revision": SOURCE_REVISION, + "vllm_integration_ref": VLLM_INTEGRATION_REF, + "case_ids": { + category: list(case_ids) for category, case_ids in SMOKE_CASE_IDS.items() + }, + } + + +def _error_dict(error: BaseException) -> dict[str, str]: + return {"type": type(error).__name__, "message": str(error)} + + +def _expected_category_details() -> list[dict[str, Any]]: + return [ + { + "category": category, + "case_ids": list(case_ids), + "score_file": None, + "score_header": None, + "score_records": [], + "case_scores": [ + {"id": case_id, "score": 0.0, "correct": False} for case_id in case_ids + ], + } + for category, case_ids in SMOKE_CASE_IDS.items() + ] + + +def _diagnostics(scores: Sequence[CategoryScore] | None = None) -> dict[str, Any]: + return { + "source": _source_details(), + "categories": ( + [score.as_dict() for score in scores] + if scores is not None + else _expected_category_details() + ), + } + + +def _native_report( + *, + model: str, + base_url: str | None, + num_threads: int, + scores: Sequence[CategoryScore] | None, + integration_error: BaseException | None = None, +) -> dict[str, Any]: + correct_count = sum(score.correct_count for score in scores or ()) + total_count = sum(score.total_count for score in scores or ()) + accuracy = correct_count / total_count if total_count else 0.0 + report: dict[str, Any] = { + "verifier": ADAPTER_NAME, + "task": TASK_NAME, + "model": model, + "endpoint": base_url, + "completed": integration_error is None, + "passed": integration_error is None and accuracy >= REQUIRED_SCORE, + "threshold": REQUIRED_SCORE, + "sampling": {"temperature": 0.0, "num_threads": num_threads}, + "summary": { + "accuracy": accuracy, + "correct_count": correct_count, + "total_count": total_count, + "expected_count": EXPECTED_SAMPLE_COUNT, + }, + "bfcl": _diagnostics(scores), + } + if integration_error is not None: + report["integration_error"] = _error_dict(integration_error) + return report + + +def _compatibility_result( + *, + model: str, + scores: Sequence[CategoryScore] | None, + integration_error: BaseException | None = None, +) -> dict[str, Any]: + score_by_category = {score.category: score for score in scores or ()} + total_count = sum(score.total_count for score in scores or ()) + correct_count = sum(score.correct_count for score in scores or ()) + accuracy = correct_count / total_count if total_count else 0.0 + task_scores = {TASK_NAME: accuracy} + task_samples = { + TASK_NAME: { + "original": EXPECTED_SAMPLE_COUNT, + "effective": total_count, + } + } + for category, case_ids in SMOKE_CASE_IDS.items(): + category_score = score_by_category.get(category) + task_name = f"bfcl_{category}" + task_scores[task_name] = ( + category_score.accuracy if category_score is not None else 0.0 + ) + task_samples[task_name] = { + "original": len(case_ids), + "effective": ( + category_score.total_count if category_score is not None else 0 + ), + } + results = { + task_name: { + "acc,none": task_score, + "acc_stderr,none": 0.0, + } + for task_name, task_score in task_scores.items() + } + configs = { + task_name: { + "metric_list": [{"metric": "acc"}], + "filter_list": [{"name": "none"}], + } + for task_name in task_scores + } + result: dict[str, Any] = { + "result_format": RESULT_FORMAT, + "eval_adapter": ADAPTER_NAME, + "model_name": model, + "results": results, + "configs": configs, + "n-samples": task_samples, + "bfcl": _diagnostics(scores), + } + if integration_error is not None: + result["integration_error"] = _error_dict(integration_error) + return result + + +def _write_json(path: Path, value: Mapping[str, Any]) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def _prepare_output_paths(output_dir: Path) -> tuple[Path, Path]: + output_dir.mkdir(parents=True, exist_ok=True) + native_path = output_dir / NATIVE_REPORT_FILENAME + native_path.unlink(missing_ok=True) + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + if stale_path.is_file() or stale_path.is_symlink(): + stale_path.unlink() + compatibility_path = output_dir / COMPATIBILITY_FILENAME + return native_path, compatibility_path + + +def _write_smoke_id_map(project_root: Path) -> None: + project_root.mkdir(parents=True, exist_ok=True) + _write_json( + project_root / "test_case_ids_to_generate.json", + {category: list(case_ids) for category, case_ids in SMOKE_CASE_IDS.items()}, + ) + + +def _function_defaults(function: Callable[..., Any]) -> dict[str, Any]: + """Resolve Typer OptionInfo defaults before directly calling a command function.""" + import typer # Lazy: only the real BFCL path needs this third-party dependency. + + defaults: dict[str, Any] = {} + for name, parameter in inspect.signature(function).parameters.items(): + if parameter.default is inspect.Parameter.empty: + continue + default = parameter.default + if isinstance(default, typer.models.OptionInfo): + default = default.default + defaults[name] = default + return defaults + + +def _run_upstream( + *, + model: str, + project_root: Path, + base_url: str, + api_key: str, + num_threads: int, + request_timeout_seconds: float, +) -> None: + """Lazily load and invoke the pinned BFCL API against an existing server.""" + os.environ["BFCL_PROJECT_ROOT"] = str(project_root) + os.environ["OPENAI_BASE_URL"] = base_url + os.environ["OPENAI_API_KEY"] = api_key + + # The adapter filename intentionally matches the installed package. When the + # file is executed directly, hide its directory during package resolution. + adapter_directory = Path(__file__).resolve().parent + original_sys_path = sys.path[:] + sys.path[:] = [ + entry + for entry in sys.path + if Path(entry or os.curdir).resolve() != adapter_directory + ] + try: + import bfcl_eval.constants.model_config as bfcl_model_config + from bfcl_eval.__main__ import evaluate, generate + from bfcl_eval.constants.model_config import ModelConfig + from bfcl_eval.model_handler.api_inference.openai_completion import ( + OpenAICompletionsHandler, + ) + finally: + sys.path[:] = original_sys_path + request_failures: SimpleQueue[Exception] = SimpleQueue() + + class BoundedOpenAICompletionsHandler(OpenAICompletionsHandler): + def _build_client_kwargs(self) -> dict[str, Any]: + kwargs = super()._build_client_kwargs() + kwargs.update(timeout=request_timeout_seconds, max_retries=0) + return kwargs + + def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: + # The upstream method has an unbounded RateLimitError retry decorator. + # The surrounding eval process owns the suite deadline, so issue once. + started = time.monotonic() + try: + response = self.client.chat.completions.create(**kwargs) + except Exception as exc: + request_failures.put(exc) + raise + return response, time.monotonic() - started + + bfcl_model_config.MODEL_CONFIG_MAPPING[model] = ModelConfig( + model_name=model, + display_name=f"{model} (FC) (InferenceX)", + url="", + org="", + license="unknown", + model_handler=BoundedOpenAICompletionsHandler, + input_price=None, + output_price=None, + is_fc_model=True, + underscore_to_dot=True, + ) + + categories = list(SMOKE_CASE_IDS) + generation_kwargs = _function_defaults(generate) + generation_kwargs.update( + model=[model], + test_category=categories, + temperature=0.0, + num_threads=num_threads, + skip_server_setup=True, + run_ids=True, + allow_overwrite=True, + ) + generate(**generation_kwargs) + if not request_failures.empty(): + raise request_failures.get() + _validate_generated_results(project_root) + + evaluation_kwargs = _function_defaults(evaluate) + evaluation_kwargs.update( + model=[model], + test_category=categories, + partial_eval=True, + ) + evaluate(**evaluation_kwargs) + + +def _validate_generated_results(project_root: Path) -> None: + for category, case_ids in SMOKE_CASE_IDS.items(): + matches = sorted(project_root.glob(f"result/**/BFCL_v4_{category}_result.json")) + if len(matches) != 1: + raise ValueError( + f"expected exactly one {category} result file, found {len(matches)}" + ) + result_ids: list[str] = [] + with matches[0].open(encoding="utf-8") as result_file: + for line_number, result_line in enumerate(result_file, start=1): + try: + record = json.loads(result_line) + except json.JSONDecodeError as exc: + raise ValueError( + f"{category} result record on line {line_number} " + "is malformed JSON" + ) from exc + if not isinstance(record, Mapping): + raise ValueError( + f"{category} result record on line {line_number} " + "must be a JSON object" + ) + case_id = record.get("id") + if not isinstance(case_id, str) or not case_id: + raise ValueError( + f"{category} result record on line {line_number} " + "must contain a non-empty string id" + ) + if case_id not in case_ids: + raise ValueError( + f"{category} result file contains unexpected id {case_id}" + ) + if case_id in result_ids: + raise ValueError( + f"{category} result file contains duplicate id {case_id}" + ) + if "traceback" in record or ( + isinstance(record.get("result"), str) + and record["result"].startswith("Error during inference:") + ): + raise RuntimeError( + f"{category} result {case_id} contains an inference error" + ) + result_ids.append(case_id) + if result_ids != list(case_ids): + raise ValueError( + f"{category} result ids {result_ids!r} " + f"do not match expected ids {list(case_ids)!r}" + ) + + +def _validate_header( + *, category: str, header: Any, expected_count: int +) -> tuple[float, int, int]: + if not isinstance(header, Mapping): + raise ValueError(f"{category} score header must be a JSON object") + missing = {"accuracy", "correct_count", "total_count"} - header.keys() + if missing: + raise ValueError( + f"{category} score header missing: {', '.join(sorted(missing))}" + ) + + correct_count = header["correct_count"] + total_count = header["total_count"] + accuracy = header["accuracy"] + if isinstance(correct_count, bool) or not isinstance(correct_count, int): + raise ValueError(f"{category} correct_count must be an integer") + if isinstance(total_count, bool) or not isinstance(total_count, int): + raise ValueError(f"{category} total_count must be an integer") + if ( + isinstance(accuracy, bool) + or not isinstance(accuracy, (int, float)) + or not math.isfinite(float(accuracy)) + ): + raise ValueError(f"{category} accuracy must be a finite number") + if total_count != expected_count: + raise ValueError( + f"{category} evaluated {total_count} cases; expected {expected_count}" + ) + if correct_count < 0 or correct_count > total_count: + raise ValueError(f"{category} correct_count is outside [0, total_count]") + + computed_accuracy = correct_count / total_count + if not math.isclose(float(accuracy), computed_accuracy, rel_tol=0.0, abs_tol=1e-12): + raise ValueError(f"{category} accuracy is inconsistent with its count fields") + return float(accuracy), correct_count, total_count + + +def _collect_scores(project_root: Path) -> list[CategoryScore]: + scores: list[CategoryScore] = [] + for category, case_ids in SMOKE_CASE_IDS.items(): + matches = sorted(project_root.glob(f"score/**/BFCL_v4_{category}_score.json")) + if len(matches) != 1: + raise ValueError( + f"expected exactly one {category} score file, found {len(matches)}" + ) + score_path = matches[0] + with score_path.open(encoding="utf-8") as score_file: + first_line = score_file.readline() + record_lines = list(score_file) + if not first_line: + raise ValueError(f"{category} score file has no header") + try: + header = json.loads(first_line) + except json.JSONDecodeError as exc: + raise ValueError(f"{category} score header is malformed JSON") from exc + accuracy, correct_count, total_count = _validate_header( + category=category, + header=header, + expected_count=len(case_ids), + ) + records: list[dict[str, Any]] = [] + record_ids: list[str] = [] + for line_number, record_line in enumerate(record_lines, start=2): + try: + record = json.loads(record_line) + except json.JSONDecodeError as exc: + raise ValueError( + f"{category} score record on line {line_number} is malformed JSON" + ) from exc + if not isinstance(record, Mapping): + raise ValueError( + f"{category} score record on line {line_number} " + "must be a JSON object" + ) + case_id = record.get("id") + if not isinstance(case_id, str) or not case_id: + raise ValueError( + f"{category} score record on line {line_number} " + "must contain a non-empty string id" + ) + if case_id not in case_ids: + raise ValueError( + f"{category} score file contains unexpected id {case_id}" + ) + if case_id in record_ids: + raise ValueError( + f"{category} score file contains duplicate id {case_id}" + ) + record_ids.append(case_id) + records.append(dict(record)) + expected_failure_count = total_count - correct_count + if len(records) != expected_failure_count: + raise ValueError( + f"{category} score file contains {len(records)} failure records; " + f"expected {expected_failure_count}" + ) + scores.append( + CategoryScore( + category=category, + case_ids=case_ids, + score_file=score_path.relative_to(project_root).as_posix(), + header=dict(header), + records=tuple(records), + accuracy=accuracy, + correct_count=correct_count, + total_count=total_count, + ) + ) + return scores + + +def publish_integration_error( + *, output_dir: Path, model: str, error: BaseException +) -> None: + """Publish required zero-score artifacts without importing BFCL or Typer.""" + native_path, compatibility_path = _prepare_output_paths(output_dir) + _write_json( + native_path, + _native_report( + model=model, + base_url=None, + num_threads=DEFAULT_NUM_THREADS, + scores=None, + integration_error=error, + ), + ) + _write_json( + compatibility_path, + _compatibility_result( + model=model, + scores=None, + integration_error=error, + ), + ) + + +def run_evaluation( + *, + base_url: str, + api_key: str, + model: str, + output_dir: Path, + bfcl_project_root: Path, + num_threads: int = DEFAULT_NUM_THREADS, + request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, + upstream_runner: UpstreamRunner = _run_upstream, +) -> bool: + """Run the exact smoke IDs and always publish native and compatibility reports.""" + native_path, compatibility_path = _prepare_output_paths(output_dir) + try: + normalized_url = _absolute_http_url(base_url) + normalized_model = _nonempty_string(model) + normalized_key = _nonempty_string(api_key) + if isinstance(num_threads, bool) or not isinstance(num_threads, int): + raise ValueError("num_threads must be a positive integer") + if num_threads <= 0: + raise ValueError("num_threads must be a positive integer") + if ( + isinstance(request_timeout_seconds, bool) + or not isinstance(request_timeout_seconds, (int, float)) + or not math.isfinite(float(request_timeout_seconds)) + or request_timeout_seconds <= 0 + ): + raise ValueError("request_timeout_seconds must be positive and finite") + if not callable(upstream_runner): + raise TypeError("upstream_runner must be callable") + + _write_smoke_id_map(bfcl_project_root) + upstream_runner( + model=normalized_model, + project_root=bfcl_project_root, + base_url=normalized_url, + api_key=normalized_key, + num_threads=num_threads, + request_timeout_seconds=float(request_timeout_seconds), + ) + _validate_generated_results(bfcl_project_root) + scores = _collect_scores(bfcl_project_root) + except Exception as exc: # noqa: BLE001 - artifact publication is the boundary + _write_json( + native_path, + _native_report( + model=model, + base_url=base_url, + num_threads=num_threads, + scores=None, + integration_error=exc, + ), + ) + _write_json( + compatibility_path, + _compatibility_result( + model=model, + scores=None, + integration_error=exc, + ), + ) + return False + + native = _native_report( + model=normalized_model, + base_url=normalized_url, + num_threads=num_threads, + scores=scores, + ) + compatibility = _compatibility_result(model=normalized_model, scores=scores) + _write_json(native_path, native) + _write_json(compatibility_path, compatibility) + return True + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run the pinned four-case BFCL V4 OpenAI completions smoke." + ) + parser.add_argument("--base-url", type=_absolute_http_url) + parser.add_argument("--api-key", type=_nonempty_string, default="EMPTY") + parser.add_argument("--model", type=_nonempty_string, required=True) + parser.add_argument("--output-dir", type=Path, required=True) + parser.add_argument("--bfcl-project-root", type=Path) + parser.add_argument( + "--num-threads", type=_positive_int, default=DEFAULT_NUM_THREADS + ) + parser.add_argument( + "--request-timeout-seconds", + type=_positive_float, + default=DEFAULT_REQUEST_TIMEOUT_SECONDS, + ) + parser.add_argument("--integration-error") + args = parser.parse_args(argv) + if args.integration_error is None: + if args.base_url is None: + parser.error("--base-url required unless --integration-error is provided") + if args.bfcl_project_root is None: + parser.error( + "--bfcl-project-root required unless --integration-error is provided" + ) + return args + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + if args.integration_error is not None: + publish_integration_error( + output_dir=args.output_dir, + model=args.model, + error=RuntimeError(args.integration_error), + ) + return 1 + + passed = run_evaluation( + base_url=args.base_url, + api_key=args.api_key, + model=args.model, + output_dir=args.output_dir, + bfcl_project_root=args.bfcl_project_root, + num_threads=args.num_threads, + request_timeout_seconds=args.request_timeout_seconds, + ) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/utils/evals/test_batched_eval.py b/utils/evals/test_batched_eval.py index 56c219e558..1c8378ca7a 100644 --- a/utils/evals/test_batched_eval.py +++ b/utils/evals/test_batched_eval.py @@ -348,3 +348,8 @@ def test_amd_multinode_container_forwards_eval_concurrency_list() -> None: ).read_text() assert 'expected_concs="${EVAL_CONC}"' in workflow assert 'validate_scores.py --expected-concs "${expected_concs}"' in workflow + assert 'if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then' in workflow + assert ( + "validate_scores.py --metric-prefix 'acc,' " + '--expected-concs "${expected_concs}"' + ) in workflow diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py new file mode 100644 index 0000000000..cc687e1cfc --- /dev/null +++ b/utils/evals/test_bfcl_eval.py @@ -0,0 +1,536 @@ +import json +import subprocess +import sys +from pathlib import Path +from typing import Any + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import bfcl_eval as be + + +def _compatibility_path(output_dir: Path) -> Path: + path = output_dir / be.COMPATIBILITY_FILENAME + assert path.exists() + return path + + +def _compatibility(output_dir: Path) -> dict[str, Any]: + return json.loads(_compatibility_path(output_dir).read_text(encoding="utf-8")) + + +def _native(output_dir: Path) -> dict[str, Any]: + return json.loads( + (output_dir / be.NATIVE_REPORT_FILENAME).read_text(encoding="utf-8") + ) + + +def _score(output_dir: Path) -> float: + return _compatibility(output_dir)["results"][be.TASK_NAME]["acc,none"] + + +def _write_result( + project_root: Path, + category: str, + rows: list[dict[str, Any]] | None = None, +) -> Path: + result_path = ( + project_root + / "result" + / "model-a" + / "nested" + / f"BFCL_v4_{category}_result.json" + ) + result_path.parent.mkdir(parents=True, exist_ok=True) + if rows is None: + rows = [{"id": be.SMOKE_CASE_IDS[category][0], "result": []}] + result_path.write_text( + "".join(json.dumps(row) + "\n" for row in rows), + encoding="utf-8", + ) + return result_path + + +def _write_score( + project_root: Path, + category: str, + correct_count: int, + *, + header_override: str | None = None, + record_id: str | None = None, +) -> None: + score_path = ( + project_root / "score" / "model-a" / "nested" / f"BFCL_v4_{category}_score.json" + ) + score_path.parent.mkdir(parents=True, exist_ok=True) + if header_override is not None: + first_line = header_override + else: + first_line = json.dumps( + { + "accuracy": float(correct_count), + "correct_count": correct_count, + "total_count": 1, + } + ) + record_text = "" + if correct_count == 0 or record_id is not None: + case_id = be.SMOKE_CASE_IDS[category][0] if record_id is None else record_id + record_text = json.dumps({"id": case_id}) + "\n" + score_path.write_text(first_line + "\n" + record_text, encoding="utf-8") + _write_result(project_root, category) + + +def _score_runner( + correct_by_category: dict[str, int] | None = None, + *, + missing: str | None = None, + malformed: str | None = None, + invocation: dict[str, Any] | None = None, +): + expected = correct_by_category or {category: 1 for category in be.SMOKE_CASE_IDS} + + def run(**kwargs: Any) -> None: + if invocation is not None: + invocation.update(kwargs) + project_root = kwargs["project_root"] + for category in be.SMOKE_CASE_IDS: + if category == missing: + _write_result(project_root, category) + continue + _write_score( + project_root, + category, + expected[category], + header_override="{not-json" if category == malformed else None, + ) + + return run + + +def _run( + tmp_path: Path, + runner, + *, + output_dir: Path | None = None, +) -> tuple[bool, Path, Path]: + output = output_dir or tmp_path / "output" + project_root = tmp_path / "bfcl-project" + passed = be.run_evaluation( + base_url="http://127.0.0.1:8000/v1/", + api_key="EMPTY", + model="model-a", + output_dir=output, + bfcl_project_root=project_root, + upstream_runner=runner, + ) + return passed, output, project_root + + +def test_command_defaults_and_required_runtime_inputs(tmp_path: Path) -> None: + args = be.parse_args( + [ + "--base-url", + "http://localhost:8000/v1/", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + ] + ) + + assert args.base_url == "http://localhost:8000/v1" + assert args.api_key == "EMPTY" + assert args.num_threads == 4 + assert args.request_timeout_seconds == 180.0 + assert args.integration_error is None + + with pytest.raises(SystemExit): + be.parse_args(["--model", "model-a", "--output-dir", str(tmp_path / "missing")]) + + +@pytest.mark.parametrize( + ("flag", "value"), + (("--num-threads", "0"), ("--request-timeout-seconds", "nan")), +) +def test_cli_rejects_invalid_positive_values( + tmp_path: Path, flag: str, value: str +) -> None: + with pytest.raises(SystemExit): + be.parse_args( + [ + "--base-url", + "http://localhost/v1", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + flag, + value, + ] + ) + + +def test_cli_rejects_chat_completions_endpoint_instead_of_api_root( + tmp_path: Path, +) -> None: + with pytest.raises(SystemExit): + be.parse_args( + [ + "--base-url", + "http://localhost/v1/chat/completions", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + ] + ) + + +def test_perfect_score_projects_pinned_ids_and_upstream_headers( + tmp_path: Path, +) -> None: + invocation: dict[str, Any] = {} + passed, output_dir, project_root = _run( + tmp_path, _score_runner(invocation=invocation) + ) + + assert passed + assert invocation == { + "model": "model-a", + "project_root": project_root, + "base_url": "http://127.0.0.1:8000/v1", + "api_key": "EMPTY", + "num_threads": 4, + "request_timeout_seconds": 180.0, + } + assert json.loads( + (project_root / "test_case_ids_to_generate.json").read_text(encoding="utf-8") + ) == { + "simple_python": ["simple_python_0"], + "multiple": ["multiple_9"], + "parallel": ["parallel_1"], + "irrelevance": ["irrelevance_0"], + } + + compatibility = _compatibility(output_dir) + native = _native(output_dir) + assert compatibility["result_format"] == "inferencex-eval-v1" + expected_tasks = [ + "bfcl_smoke", + "bfcl_simple_python", + "bfcl_multiple", + "bfcl_parallel", + "bfcl_irrelevance", + ] + assert list(compatibility["results"]) == expected_tasks + assert all( + compatibility["results"][task_name] == {"acc,none": 1.0, "acc_stderr,none": 0.0} + for task_name in expected_tasks + ) + assert all( + compatibility["configs"][task_name] + == { + "metric_list": [{"metric": "acc"}], + "filter_list": [{"name": "none"}], + } + for task_name in expected_tasks + ) + assert compatibility["n-samples"][be.TASK_NAME] == { + "original": 4, + "effective": 4, + } + assert all( + compatibility["n-samples"][f"bfcl_{category}"] + == {"original": 1, "effective": 1} + for category in be.SMOKE_CASE_IDS + ) + assert compatibility["bfcl"]["source"]["package_version"] == "2026.3.23" + assert compatibility["bfcl"]["source"]["wheel_sha256"] == ( + "3bb6dfa5f0c68ad403c9ec50b00db2bb3b4cc9b38ab1ff33f48fe30d853d3a0a" + ) + assert compatibility["bfcl"]["source"]["source_revision"] == ( + "6ea57973c7a6097fd7c5915698c54c17c5b1b6c8" + ) + assert [entry["category"] for entry in compatibility["bfcl"]["categories"]] == [ + "simple_python", + "multiple", + "parallel", + "irrelevance", + ] + assert all( + entry["score_header"] == {"accuracy": 1.0, "correct_count": 1, "total_count": 1} + for entry in compatibility["bfcl"]["categories"] + ) + assert all( + entry["score_records"] == [] for entry in compatibility["bfcl"]["categories"] + ) + assert native["completed"] is True + assert native["passed"] is True + assert native["summary"] == { + "accuracy": 1.0, + "correct_count": 4, + "total_count": 4, + "expected_count": 4, + } + assert native["bfcl"] == compatibility["bfcl"] + assert sorted(path.name for path in output_dir.iterdir()) == [ + "bfcl_report.json", + "results_bfcl.json", + ] + + +def test_weighted_score_failure_is_complete_and_left_to_threshold_validator( + tmp_path: Path, +) -> None: + completed, output_dir, _ = _run( + tmp_path, + _score_runner( + { + "simple_python": 1, + "multiple": 0, + "parallel": 0, + "irrelevance": 1, + } + ), + ) + + assert completed + assert _score(output_dir) == 0.5 + compatibility = _compatibility(output_dir) + assert compatibility["results"]["bfcl_multiple"]["acc,none"] == 0.0 + assert compatibility["results"]["bfcl_parallel"]["acc,none"] == 0.0 + assert compatibility["n-samples"][be.TASK_NAME]["effective"] == 4 + assert "integration_error" not in compatibility + native = _native(output_dir) + assert native["completed"] is True + assert native["passed"] is False + assert native["threshold"] == 0.75 + assert native["summary"]["correct_count"] == 2 + + +def test_stale_compatibility_outputs_are_removed_without_touching_foreign_results( + tmp_path: Path, +) -> None: + output_dir = tmp_path / "output" + output_dir.mkdir() + stale = output_dir / "results_bfcl_2000-01-01T00-00-00.000000.json" + stale.write_text("{}", encoding="utf-8") + stale_native = output_dir / be.NATIVE_REPORT_FILENAME + stale_native.write_text("{}", encoding="utf-8") + foreign = output_dir / "results_other_eval.json" + foreign.write_text("{}", encoding="utf-8") + + passed, _, _ = _run(tmp_path, _score_runner(), output_dir=output_dir) + + assert passed + assert not stale.exists() + assert foreign.exists() + assert len(list(output_dir.glob(be.COMPATIBILITY_GLOB))) == 1 + assert _native(output_dir)["summary"]["accuracy"] == 1.0 + + +def test_swallowed_inference_error_is_an_integration_failure( + tmp_path: Path, +) -> None: + base_runner = _score_runner() + + def runner(**kwargs: Any) -> None: + base_runner(**kwargs) + _write_result( + kwargs["project_root"], + "parallel", + [ + { + "id": "parallel_1", + "result": "Error during inference: request timed out", + "traceback": "TimeoutError: request timed out", + } + ], + ) + + completed, output_dir, _ = _run(tmp_path, runner) + + assert not completed + assert _score(output_dir) == 0.0 + assert _compatibility(output_dir)["integration_error"] == { + "type": "RuntimeError", + "message": "parallel result parallel_1 contains an inference error", + } + + +@pytest.mark.parametrize( + ("mode", "expected_message"), + ( + ( + "missing", + "parallel result ids [] do not match expected ids ['parallel_1']", + ), + ("duplicate", "parallel result file contains duplicate id parallel_1"), + ), +) +def test_missing_or_duplicate_generated_id_is_an_integration_failure( + tmp_path: Path, + mode: str, + expected_message: str, +) -> None: + base_runner = _score_runner() + + def runner(**kwargs: Any) -> None: + base_runner(**kwargs) + row = {"id": "parallel_1", "result": []} + _write_result( + kwargs["project_root"], + "parallel", + [] if mode == "missing" else [row, row], + ) + + completed, output_dir, _ = _run(tmp_path, runner) + + assert not completed + assert _score(output_dir) == 0.0 + assert _compatibility(output_dir)["integration_error"] == { + "type": "ValueError", + "message": expected_message, + } + + +def test_missing_category_is_an_integration_failure(tmp_path: Path) -> None: + passed, output_dir, _ = _run(tmp_path, _score_runner(missing="parallel")) + + assert not passed + compatibility = _compatibility(output_dir) + assert _score(output_dir) == 0.0 + assert compatibility["n-samples"][be.TASK_NAME]["effective"] == 0 + assert compatibility["integration_error"]["type"] == "ValueError" + assert "parallel score file" in compatibility["integration_error"]["message"] + assert _native(output_dir)["completed"] is False + + +def test_malformed_score_header_is_an_integration_failure(tmp_path: Path) -> None: + passed, output_dir, _ = _run(tmp_path, _score_runner(malformed="multiple")) + + assert not passed + compatibility = _compatibility(output_dir) + assert _score(output_dir) == 0.0 + assert compatibility["integration_error"] == { + "type": "ValueError", + "message": "multiple score header is malformed JSON", + } + + +def test_unexpected_score_record_id_is_an_integration_failure( + tmp_path: Path, +) -> None: + def runner(**kwargs: Any) -> None: + project_root = kwargs["project_root"] + for category in be.SMOKE_CASE_IDS: + _write_score( + project_root, + category, + 0 if category == "parallel" else 1, + record_id="parallel_0" if category == "parallel" else None, + ) + + completed, output_dir, _ = _run(tmp_path, runner) + + assert not completed + compatibility = _compatibility(output_dir) + assert _score(output_dir) == 0.0 + assert compatibility["integration_error"] == { + "type": "ValueError", + "message": "parallel score file contains unexpected id parallel_0", + } + + +def test_incomplete_score_header_is_an_integration_failure(tmp_path: Path) -> None: + def runner(**kwargs: Any) -> None: + project_root = kwargs["project_root"] + for category in be.SMOKE_CASE_IDS: + if category == "irrelevance": + _write_score( + project_root, + category, + 1, + header_override=json.dumps({"accuracy": 1.0, "correct_count": 1}), + ) + else: + _write_score(project_root, category, 1) + + passed, output_dir, _ = _run(tmp_path, runner) + + assert not passed + assert _compatibility(output_dir)["integration_error"]["message"] == ( + "irrelevance score header missing: total_count" + ) + + +def test_upstream_exception_publishes_zero_score_reports(tmp_path: Path) -> None: + def fail(**kwargs: Any) -> None: + raise RuntimeError("BFCL generation failed") + + passed, output_dir, project_root = _run(tmp_path, fail) + + assert not passed + assert (project_root / "test_case_ids_to_generate.json").exists() + assert _score(output_dir) == 0.0 + compatibility = _compatibility(output_dir) + assert compatibility["integration_error"] == { + "type": "RuntimeError", + "message": "BFCL generation failed", + } + assert compatibility["bfcl"]["source"]["case_ids"] == { + "simple_python": ["simple_python_0"], + "multiple": ["multiple_9"], + "parallel": ["parallel_1"], + "irrelevance": ["irrelevance_0"], + } + assert _native(output_dir)["summary"]["total_count"] == 0 + + +def test_integration_error_cli_is_stdlib_only_and_returns_nonzero( + tmp_path: Path, +) -> None: + output_dir = tmp_path / "output" + script = Path(be.__file__).resolve() + + completed = subprocess.run( + [ + sys.executable, + "-I", + str(script), + "--model", + "model-a", + "--output-dir", + str(output_dir), + "--integration-error", + "pinned wheel installation failed", + ], + check=False, + capture_output=True, + text=True, + ) + + assert completed.returncode == 1 + assert completed.stderr == "" + assert _score(output_dir) == 0.0 + compatibility = _compatibility(output_dir) + assert compatibility["n-samples"][be.TASK_NAME] == { + "original": 4, + "effective": 0, + } + assert compatibility["integration_error"] == { + "type": "RuntimeError", + "message": "pinned wheel installation failed", + } + native = _native(output_dir) + assert native["completed"] is False + assert native["integration_error"] == compatibility["integration_error"] diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 640a2cbcfb..1c2866e022 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -31,6 +31,7 @@ run_swebench_eval() { echo "DISPATCH=swebench"; } run_kimi_vendor_eval() { echo "DISPATCH=kimi-vendor"; } run_minimax_vendor_eval() { echo "DISPATCH=minimax-vendor"; } +run_bfcl_eval() { echo "DISPATCH=bfcl"; } append_lm_eval_summary() { echo "STAGED=summary"; } export EVAL_MAX_MODEL_LEN=16384 export EVAL_CONCURRENT_REQUESTS="" @@ -55,6 +56,7 @@ def _dispatch( env.pop("EVAL_FRAMEWORK", None) env.pop("CLI_FW", None) env.pop("KV_OFFLOAD_BACKEND", None) + env.pop("EVAL_SUITE", None) if cli_fw is not None: env["CLI_FW"] = cli_fw if env_fw is not None: @@ -198,7 +200,7 @@ def test_run_eval_rejects_suite_override_for_lm_eval() -> None: ) assert result.returncode == 2 - assert "only supported with a provider verifier framework" in result.stderr + assert "only supported with kimi-vendor, minimax-vendor, or bfcl" in result.stderr def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: @@ -2019,6 +2021,7 @@ def test_fixed_eval_workflows_forward_provider_contract() -> None: assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" assert "*_vendor_report.json" in SINGLE_NODE_WORKFLOW.read_text() + assert "bfcl_report.json" in SINGLE_NODE_WORKFLOW.read_text() @@ -2032,6 +2035,7 @@ def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" assert "*_vendor_report.json" in MULTINODE_WORKFLOW.read_text() + assert "bfcl_report.json" in MULTINODE_WORKFLOW.read_text() @@ -2051,4 +2055,375 @@ def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: assert '"multinode_agentic_evals"' in flatten_command get_jobs_command = get_jobs["run"] assert "EVALS=$(" in get_jobs_command - assert "score_matrix eval" in get_jobs_command \ No newline at end of file + assert "score_matrix eval" in get_jobs_command + + +def test_env_can_force_bfcl_on_agentic_eval() -> None: + output = _dispatch(is_agentic="1", eval_only="true", env_fw="bfcl") + + assert "DISPATCH=bfcl" in output + assert "STAGED=summary" in output + +def test_cli_can_force_bfcl_on_fixed_seqlen_eval() -> None: + output = _dispatch(is_agentic="0", cli_fw="bfcl") + + assert "DISPATCH=bfcl" in output + assert "STAGED=summary" not in output + +def test_bfcl_defaults_suite_dispatches_once_without_context_loading() -> None: + script = r""" +source "$BENCHMARK_LIB" +unset EVAL_MAX_MODEL_LEN +compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } +BFCL_DISPATCH_COUNT=0 +run_bfcl_eval() { + BFCL_DISPATCH_COUNT=$((BFCL_DISPATCH_COUNT + 1)) + printf 'DISPATCH=bfcl SUITE=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" +} +append_lm_eval_summary() { printf 'STAGED=%s\n' "$EVAL_COMPLETED_SUITE"; } +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +run_eval --framework bfcl --port 9999 +printf 'DISPATCH_COUNT=%s\n' "$BFCL_DISPATCH_COUNT" +printf 'COMPLETED_SUITE=%s\n' "$EVAL_COMPLETED_SUITE" +""" + env = {**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "MODEL": "served-model"} + for key in ("EVAL_FRAMEWORK", "EVAL_SUITE", "EVAL_COMPLETED_SUITE"): + env.pop(key, None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + + assert "DISPATCH=bfcl SUITE=bfcl_smoke ARGS=<--port 9999>" in result.stdout + assert "DISPATCH_COUNT=1" in result.stdout + assert "COMPLETED_SUITE=bfcl_smoke" in result.stdout + assert "STAGED=bfcl_smoke" not in result.stdout + assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout + +def test_bfcl_rejects_suite_from_another_provider() -> None: + result = _run_invalid_call( + "EVAL_CONCURRENT_REQUESTS='' " + "EVAL_SUITE=minimax_m3_smoke " + "run_eval --framework bfcl" + ) + + assert result.returncode == 2 + assert "unsupported BFCL suite 'minimax_m3_smoke'" in result.stderr + +def test_bfcl_suite_is_rejected_by_mismatched_framework() -> None: + result = _run_invalid_call( + "EVAL_CONCURRENT_REQUESTS='' " + "EVAL_SUITE=bfcl_smoke " + "run_eval --framework minimax-vendor" + ) + + assert result.returncode == 2 + assert "unsupported MiniMax Provider Verifier suite 'bfcl_smoke'" in result.stderr + +def test_bfcl_rejects_unknown_suite() -> None: + result = _run_invalid_call("EVAL_SUITE=not_a_bfcl_suite run_bfcl_eval") + + assert result.returncode == 2 + assert "unsupported BFCL suite 'not_a_bfcl_suite'" in result.stderr + +def test_bfcl_dependency_timeout_uses_integration_error_and_stages( + tmp_path: Path, +) -> None: + results_dir = tmp_path / "results" + python_dir = tmp_path / "python" + script = r""" +source "$BENCHMARK_LIB" +unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE +unset VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +selected_python() { + printf 'ADAPTER_ARG=<%s>\n' "$@" + touch "$RESULTS_DIR/bfcl_report.json" "$RESULTS_DIR/results_bfcl.json" + return 1 +} +_prepare_vendor_verifier_python() { + mkdir "$PYTHON_DIR" + VENDOR_VERIFIER_PYTHON=selected_python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +} +_prepare_bfcl_runtime() { return 124; } +python3() { echo "UNEXPECTED_SYSTEM_PYTHON"; return 99; } +append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR"; } +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +eval_rc=0 +run_eval --framework bfcl --results-dir "$RESULTS_DIR" || eval_rc=$? +printf 'EVAL_RC=%s\n' "$eval_rc" +exit "$eval_rc" +""" + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "PYTHON_DIR": str(python_dir), + "MODEL": "test-model", + } + env.pop("EVAL_FRAMEWORK", None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=False, + ) + output = result.stdout + result.stderr + + assert result.returncode == 124 + assert "EVAL_RC=124" in output + assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/bfcl_eval.py'}>" in output + assert "ADAPTER_ARG=" in output + assert f"ADAPTER_ARG=<{results_dir}>" in output + assert "ADAPTER_ARG=<--integration-error>" in output + assert ( + "ADAPTER_ARG=" in output + ) + assert output.count(f"STAGED=<{results_dir}>") == 1 + assert (results_dir / "bfcl_report.json").exists() + assert (results_dir / "results_bfcl.json").exists() + assert "failed to write BFCL failure artifact" not in output + assert "UNEXPECTED_SYSTEM_PYTHON" not in output + assert not python_dir.exists() + +def _run_bfcl_adapter_command( + tmp_path: Path, *, adapter_rc: int = 0 +) -> tuple[subprocess.CompletedProcess[str], tuple[Path, Path, Path, Path]]: + results_dir = tmp_path / "results" + runtime_dir = tmp_path / "runtime" + python_dir = tmp_path / "python" + project_root = tmp_path / "bfcl-project" + script = r""" +source "$BENCHMARK_LIB" +selected_python() { + printf 'ADAPTER_ARG=<%s>\n' "$@" + local arg + for arg in "$@"; do + if [ "$arg" = "--integration-error" ]; then + touch "$RESULTS_DIR/bfcl_report.json" "$RESULTS_DIR/results_bfcl.json" + return 1 + fi + done + return "$TEST_ADAPTER_RC" +} +_prepare_vendor_verifier_python() { + printf 'PREPARE_ARG=<%s>\n' "$@" + mkdir "$PYTHON_DIR" + VENDOR_VERIFIER_PYTHON=selected_python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +} +_prepare_bfcl_runtime() { + mkdir "$RUNTIME_DIR" + printf '%s\n' "$RUNTIME_DIR" +} +mktemp() { + printf 'MKTEMP_ARG=<%s>\n' "$@" >&2 + mkdir "$PROJECT_ROOT" + printf '%s\n' "$PROJECT_ROOT" +} +timeout() { + printf 'TIMEOUT_ARG=<%s>\n' "$1" + shift + "$@" +} +append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR"; } +unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE EVAL_MAX_MODEL_LEN +compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } +export EVAL_CONCURRENT_REQUESTS="" +export EVAL_ONLY=false +export IS_AGENTIC=0 +eval_rc=0 +run_eval --framework bfcl --port 9999 --results-dir "$RESULTS_DIR" || eval_rc=$? +printf 'EVAL_RC=%s\n' "$eval_rc" +printf 'EVAL_COMPLETED_SUITE=%s\n' "$EVAL_COMPLETED_SUITE" +printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" +exit "$eval_rc" +""" + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "RUNTIME_DIR": str(runtime_dir), + "PYTHON_DIR": str(python_dir), + "PROJECT_ROOT": str(project_root), + "MODEL": "repository/model", + "MODEL_NAME": "served-model", + "OPENAI_API_KEY": "must-not-be-forwarded", + "TEST_ADAPTER_RC": str(adapter_rc), + } + for key in ( + "EVAL_FRAMEWORK", + "EVAL_SUITE", + "EVAL_RESULT_DIR", + "EVAL_COMPLETED_SUITE", + "VENDOR_VERIFIER_PYTHON", + "VENDOR_VERIFIER_PYTHON_CLEANUP_DIR", + ): + env.pop(key, None) + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=False, + ) + return result, (results_dir, runtime_dir, python_dir, project_root) + +def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( + tmp_path: Path, +) -> None: + result, paths = _run_bfcl_adapter_command(tmp_path) + results_dir, runtime_dir, python_dir, project_root = paths + output = result.stdout + result.stderr + + assert result.returncode == 0, result.stderr + for value in ( + str(REPO_ROOT / "utils/evals/bfcl_eval.py"), + "--base-url", + "http://127.0.0.1:9999/v1", + "--api-key", + "EMPTY", + "--model", + "served-model", + "--output-dir", + str(results_dir), + "--bfcl-project-root", + str(project_root), + "--num-threads", + "4", + "--request-timeout-seconds", + "180", + ): + assert f"ADAPTER_ARG=<{value}>" in output + assert "PREPARE_ARG=" in output + assert "PREPARE_ARG=" in output + assert "PREPARE_ARG=" in output + assert "PREPARE_ARG=<10>" in output + assert "TIMEOUT_ARG=<900>" in output + assert "ADAPTER_ARG=" not in output + assert "UNEXPECTED_CONTEXT_LOAD" not in output + assert f"STAGED=<{results_dir}>" not in output + assert "EVAL_RC=0" in output + assert "EVAL_COMPLETED_SUITE=bfcl_smoke" in output + assert f"EVAL_RESULT_DIR={results_dir}" in output + assert results_dir.exists() + assert not runtime_dir.exists() + assert not python_dir.exists() + assert not project_root.exists() + +def test_bfcl_adapter_timeout_writes_reports_stages_and_propagates( + tmp_path: Path, +) -> None: + result, paths = _run_bfcl_adapter_command(tmp_path, adapter_rc=124) + results_dir, runtime_dir, python_dir, project_root = paths + output = result.stdout + result.stderr + + assert result.returncode == 124 + assert "EVAL_RC=124" in output + assert output.count(f"STAGED=<{results_dir}>") == 1 + assert "ADAPTER_ARG=<--integration-error>" in output + assert "ADAPTER_ARG=" in output + assert (results_dir / "bfcl_report.json").exists() + assert (results_dir / "results_bfcl.json").exists() + assert "failed to write BFCL failure artifact" not in output + assert "run_eval failed with exit code 124" in result.stderr + assert not runtime_dir.exists() + assert not python_dir.exists() + assert not project_root.exists() + +def test_bfcl_installer_uses_verified_wheel_in_selected_venv(tmp_path: Path) -> None: + script = r""" +source "$BENCHMARK_LIB" +selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +timeout() { + printf 'TIMEOUT_ARG=<%s>\n' "$1" + shift + "$@" +} +VENDOR_VERIFIER_PYTHON=selected_python +_install_bfcl_eval_deps "$DOWNLOAD_DIR" +""" + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "DOWNLOAD_DIR": str(tmp_path), + }, + text=True, + capture_output=True, + check=True, + ) + wheel_path = tmp_path / "bfcl_eval-2026.3.23-py3-none-any.whl" + + for value in ( + "https://files.pythonhosted.org/packages/ba/41/" + "ed458527c770c50225b60bae3b0c3444b26804ee455fa2d8f187018d2cb2/" + "bfcl_eval-2026.3.23-py3-none-any.whl", + "3bb6dfa5f0c68ad403c9ec50b00db2bb3b4cc9b38ab1ff33f48fe30d853d3a0a", + str(wheel_path), + "-m", + "pip", + "install", + "--no-cache-dir", + ): + assert f"PYTHON_ARG=<{value}>" in result.stdout + assert "TIMEOUT_ARG=<600>" in result.stdout + assert "--break-system-packages" not in result.stdout + assert "--target" not in result.stdout + +def test_bfcl_python_preparation_exposes_system_site_packages( + tmp_path: Path, +) -> None: + python_root = tmp_path / "bfcl-python" + script = r""" +source "$BENCHMARK_LIB" +python3() { + if [ "$1" = "-c" ]; then + printf 'VERSION_CHECK_ARG=<%s>\n' "$@" + return 0 + fi + printf 'SYSTEM_PYTHON_ARG=<%s>\n' "$@" + venv_dir="${!#}" + mkdir -p "$venv_dir/bin" + printf '#!/usr/bin/env bash\n' > "$venv_dir/bin/python" + chmod +x "$venv_dir/bin/python" +} +mktemp() { + mkdir "$PYTHON_ROOT" + printf '%s\n' "$PYTHON_ROOT" +} +_prepare_vendor_verifier_python "BFCL" "bfcl-python" true 10 +cleanup_dir="$VENDOR_VERIFIER_PYTHON_CLEANUP_DIR" +printf 'SELECTED_PYTHON=<%s>\n' "$VENDOR_VERIFIER_PYTHON" +_cleanup_vendor_eval "$cleanup_dir" +""" + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "PYTHON_ROOT": str(python_root), + }, + text=True, + capture_output=True, + check=True, + ) + + assert "SYSTEM_PYTHON_ARG=<-m>" in result.stdout + assert "SYSTEM_PYTHON_ARG=" in result.stdout + assert "SYSTEM_PYTHON_ARG=<--system-site-packages>" in result.stdout + assert "VERSION_CHECK_ARG=<10>" in result.stdout + assert f"SELECTED_PYTHON=<{python_root / 'venv/bin/python'}>" in result.stdout + assert "--prefix" not in result.stdout + assert not python_root.exists() diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index 8adb806b50..04c98613ad 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -3,6 +3,12 @@ default: gsm8k: 0.90 kimi_tool_call_schema: 1.0 minimax_m3_smoke: 1.0 + # The aggregate gates at 3/4 fixed cases; per-category rows are diagnostic. + bfcl_smoke: 0.75 + bfcl_simple_python: 0.0 + bfcl_multiple: 0.0 + bfcl_parallel: 0.0 + bfcl_irrelevance: 0.0 gpqa_diamond_cot_n_shot: 0.30 swebench_lite: 0.50 models: diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index de7a27b850..081a10a6ba 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -340,4 +340,58 @@ def test_collect_eval_rows_uses_extract_filter_as_primary_score( assert len(rows) == 1 assert rows[0]["score"] == 0.75 - assert rows[0]["score_name"] == "em_flexible" \ No newline at end of file + assert rows[0]["score_name"] == "em_flexible" + + +def test_collect_eval_rows_accepts_bfcl_compatibility_and_ignores_native_report( + tmp_path: Path, +) -> None: + artifact_dir = tmp_path / "eval_bfcl" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "bfcl_smoke"}) + ) + (artifact_dir / "bfcl_report.json").write_text( + json.dumps( + { + "results": {"native_only": {"acc,none": 1.0}}, + } + ) + ) + compatibility_path = artifact_dir / "results_bfcl.json" + tasks = { + "bfcl_smoke": 0.75, + "bfcl_simple_python": 1.0, + } + compatibility_path.write_text( + json.dumps( + { + "result_format": EVAL_RESULT_FORMAT, + "model_name": "test-model", + "results": { + task: { + "acc,none": score, + "acc_stderr,none": 0.0, + } + for task, score in tasks.items() + }, + "configs": { + task: { + "metric_list": [{"metric": "acc"}], + "filter_list": [{"name": "none"}], + } + for task in tasks + }, + "n-samples": { + "bfcl_smoke": {"effective": 4}, + "bfcl_simple_python": {"effective": 1}, + }, + } + ) + ) + + rows = collect_eval_rows(tmp_path) + + assert {row["task"]: row["score"] for row in rows} == tasks + assert {row["score_name"] for row in rows} == {"accuracy"} + assert {row["source"] for row in rows} == {str(compatibility_path)} From 9fff29e713d01c4a32f02662a99c3ff878b578fe Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 03:58:35 -0500 Subject: [PATCH 48/99] fix: install bfcl audio dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install the undeclared soundfile dependency required while importing BFCL model handlers. 中文:安装 BFCL 模型处理器导入时所需但上游未声明的 soundfile 依赖。 --- benchmarks/benchmark_lib.sh | 2 +- utils/evals/test_run_eval_dispatch.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 4087378d01..61a68bf233 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1386,7 +1386,7 @@ except Exception as error: PY timeout 600 "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install \ - -q --no-cache-dir "$wheel_path" + -q --no-cache-dir "$wheel_path" "soundfile==0.13.1" } _prepare_bfcl_runtime() { diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1c2866e022..69ab3d5dd2 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2376,6 +2376,7 @@ def test_bfcl_installer_uses_verified_wheel_in_selected_venv(tmp_path: Path) -> "pip", "install", "--no-cache-dir", + "soundfile==0.13.1", ): assert f"PYTHON_ARG=<{value}>" in result.stdout assert "TIMEOUT_ARG=<600>" in result.stdout From 25507335866932be48259378e4dd7d48f5930c16 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 04:10:39 -0500 Subject: [PATCH 49/99] fix: require externally grounded bfcl calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace directly answerable arithmetic cases with current-price and library-search cases that require tool selection and preserve typed arguments. 中文:将可直接回答的算术样例替换为必须选择工具的实时价格与图书馆检索样例,并保留参数类型。 --- utils/evals/EVALS.md | 13 +++++++------ utils/evals/bfcl_eval.py | 4 ++-- utils/evals/test_bfcl_eval.py | 8 ++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 0d9ae33f75..86280fa3df 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -218,15 +218,16 @@ but uses a fixed four-case V4 partial evaluation: | BFCL category | Exact upstream case ID | Projected task | |---------------|------------------------|----------------| -| `simple_python` | `simple_python_0` | `bfcl_simple_python` | -| `multiple` | `multiple_9` | `bfcl_multiple` | +| `simple_python` | `simple_python_141` | `bfcl_simple_python` | +| `multiple` | `multiple_38` | `bfcl_multiple` | | `parallel` | `parallel_1` | `bfcl_parallel` | | `irrelevance` | `irrelevance_0` | `bfcl_irrelevance` | -The verified wheel is installed into a temporary Python 3.10-or-newer virtual -environment with system site packages enabled so the image's existing -Torch/Transformers stack can be reused; it never mutates the global Python -environment. The temporary environment and BFCL project root are removed after +The verified wheel and its undeclared `soundfile==0.13.1` import dependency are +installed into a temporary Python 3.10-or-newer virtual environment with system +site packages enabled so the image's existing Torch/Transformers stack can be +reused; it never mutates the global Python environment. The temporary +environment and BFCL project root are removed after the run. Once package installation finishes, evaluation is local-only: BFCL skips its server setup and uses only the already-running local API root, typically `http://127.0.0.1:$PORT/v1`. The OpenAI SDK appends diff --git a/utils/evals/bfcl_eval.py b/utils/evals/bfcl_eval.py index 692e2a7736..ada0de455b 100755 --- a/utils/evals/bfcl_eval.py +++ b/utils/evals/bfcl_eval.py @@ -38,8 +38,8 @@ # Dict insertion order is intentional: reports and the upstream run-ID file are stable. SMOKE_CASE_IDS: dict[str, tuple[str, ...]] = { - "simple_python": ("simple_python_0",), - "multiple": ("multiple_9",), + "simple_python": ("simple_python_141",), + "multiple": ("multiple_38",), "parallel": ("parallel_1",), "irrelevance": ("irrelevance_0",), } diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index cc687e1cfc..d4dea2944b 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -215,8 +215,8 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( assert json.loads( (project_root / "test_case_ids_to_generate.json").read_text(encoding="utf-8") ) == { - "simple_python": ["simple_python_0"], - "multiple": ["multiple_9"], + "simple_python": ["simple_python_141"], + "multiple": ["multiple_38"], "parallel": ["parallel_1"], "irrelevance": ["irrelevance_0"], } @@ -488,8 +488,8 @@ def fail(**kwargs: Any) -> None: "message": "BFCL generation failed", } assert compatibility["bfcl"]["source"]["case_ids"] == { - "simple_python": ["simple_python_0"], - "multiple": ["multiple_9"], + "simple_python": ["simple_python_141"], + "multiple": ["multiple_38"], "parallel": ["parallel_1"], "irrelevance": ["irrelevance_0"], } From 2dec376abe13df4b78506ad1ed6e428e8c1169b1 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:30:17 -0500 Subject: [PATCH 50/99] fix: harden tool eval failure handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve verifier diagnostics across launchers, enforce safe reusable-artifact identity, validate BFCL metrics without optional YAML dependencies, and retry only transient Kimi endpoint failures. 中文:强化工具调用评估的失败处理。跨启动器保留验证器诊断信息,严格校验可复用评估产物身份,在缺少可选 YAML 依赖时仍能校验 BFCL 指标,并且仅重试 Kimi 端点的瞬态错误。 --- benchmarks/benchmark_lib.sh | 6 +- .../multi_node/amd_utils/server_atom.sh | 2 + .../multi_node/amd_utils/server_sglang.sh | 2 + .../multi_node/amd_utils/server_vllm.sh | 2 + runners/patch_srt_eval_dispatch.py | 6 +- runners/test_slurm_utils.py | 5 +- utils/evals/EVALS.md | 9 ++- utils/evals/kimi_vendor_eval.py | 9 ++- utils/evals/minimax_provider_eval.py | 4 +- utils/evals/test_bfcl_eval.py | 17 +++++ utils/evals/test_kimi_vendor_eval.py | 9 ++- utils/evals/test_minimax_provider_eval.py | 8 ++ utils/evals/test_run_eval_dispatch.py | 8 +- utils/evals/thresholds.yaml | 70 ++++++++++-------- .../test_validate_reusable_sweep_artifacts.py | 73 +++++++++++++++++-- utils/validate_reusable_sweep_artifacts.py | 42 ++++++++--- 16 files changed, 208 insertions(+), 64 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 61a68bf233..0d850cc53d 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1561,7 +1561,7 @@ _write_minimax_vendor_integration_error() { # The adapter's integration-error path is stdlib-only, so it remains usable # when Python provisioning or dependency installation is what failed. - python3 "$adapter_path" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ --model "$model_name" \ --output-dir "$results_dir" \ --integration-error "$message" @@ -2577,6 +2577,10 @@ run_eval() { return 0 fi + if [ -n "${EVAL_CONCURRENT_REQUESTS:-}" ]; then + export CONC="$EVAL_CONCURRENT_REQUESTS" + fi + local eval_rc=0 case "$framework" in lm-eval|lm_eval) run_lm_eval "${forwarded[@]}" || eval_rc=$? ;; diff --git a/benchmarks/multi_node/amd_utils/server_atom.sh b/benchmarks/multi_node/amd_utils/server_atom.sh index 35483a3667..0ddb04ce29 100755 --- a/benchmarks/multi_node/amd_utils/server_atom.sh +++ b/benchmarks/multi_node/amd_utils/server_atom.sh @@ -439,6 +439,8 @@ if [ "$NODE_RANK" -eq 0 ]; then [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" done find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" diff --git a/benchmarks/multi_node/amd_utils/server_sglang.sh b/benchmarks/multi_node/amd_utils/server_sglang.sh index 45b5ca9647..762a999eb4 100755 --- a/benchmarks/multi_node/amd_utils/server_sglang.sh +++ b/benchmarks/multi_node/amd_utils/server_sglang.sh @@ -1138,6 +1138,8 @@ print(json.dumps(json.loads(sys.stdin.read())))' <<<"$_val")" || { [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" done find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" diff --git a/benchmarks/multi_node/amd_utils/server_vllm.sh b/benchmarks/multi_node/amd_utils/server_vllm.sh index c743444c1a..f2469bdf81 100755 --- a/benchmarks/multi_node/amd_utils/server_vllm.sh +++ b/benchmarks/multi_node/amd_utils/server_vllm.sh @@ -390,6 +390,8 @@ if [ "$NODE_RANK" -eq 0 ]; then [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" done find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; + find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index fa2697ac65..a66b0a5a2d 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -20,8 +20,8 @@ GENERIC_EVAL_COMMAND = 'run_eval --port "$PORT" || eval_rc=$?' EVAL_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" -KIMI_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true -cp -v *_vendor_report.json /logs/eval_results/ 2>/dev/null || true +VERIFIER_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true +cp -v *_vendor_report.json bfcl_report.json /logs/eval_results/ 2>/dev/null || true cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" @@ -65,7 +65,7 @@ def patch_checkout(root: Path) -> list[Path]: root / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh", ( (LM_EVAL_COMMAND, GENERIC_EVAL_COMMAND), - (EVAL_ARTIFACT_COPY, KIMI_ARTIFACT_COPY), + (EVAL_ARTIFACT_COPY, VERIFIER_ARTIFACT_COPY), ), ), ) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 3fb037398a..cc8c501f15 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -105,6 +105,7 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert 'run_eval --port "$PORT"' in eval_script.read_text() assert "--framework lm-eval" not in eval_script.read_text() assert "*_vendor_report.json" in eval_script.read_text() + assert "bfcl_report.json" in eval_script.read_text() assert "already patched" in second.stdout @@ -307,7 +308,7 @@ def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: assert compilation_config["pass_config"]["fuse_allreduce_rms"] is False -def test_gb200_dynamo_kimi_recipes_enable_structural_tool_constraints() -> None: +def test_gb200_dynamo_kimi_recipes_configure_tool_parser() -> None: recipe_dir = ( REPO_ROOT / "benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" ) @@ -322,7 +323,7 @@ def test_gb200_dynamo_kimi_recipes_enable_structural_tool_constraints() -> None: assert frontend["type"] == "dynamo" config = recipe["backend"]["vllm_config"]["aggregated"] assert config["dyn-tool-call-parser"] == "kimi_k3" - assert config["dyn-enable-structural-tag"] is True + def test_b200_kimi_recipe_uses_available_roce_devices() -> None: diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 86280fa3df..76e3955cde 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -92,7 +92,7 @@ upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; - `--think-mode none` for other models, or `--think-mode opensource --thinking` for `dsv4`, plus `--selection object --max-cases 1 --max-tokens 2048`; -- the upstream-recommended `--reruns 3 --reruns-delay 2`; +- up to six three-second reruns, limited to transient HTTP and transport errors; - the bundled Walle case directory and `--tool-json-report`. The temporary Python runtime, package directory, and verifier checkout are @@ -100,9 +100,10 @@ removed after both successful and failed runs. The selection is `TestAdditionalProperties:1`, parametrized upstream in non-streaming and streaming modes. Pytest makes one initial attempt and up to -three reruns of each failing mode, with a two-second delay before each rerun. -These retries reduce transient transport and model-sampling flakes; they do not -make the smoke deterministic. The unchanged native report remains one final +six reruns of each mode at three-second intervals, but only for HTTP 404, 429, +5xx, connection, and timeout failures. This covers frontends whose health route +becomes ready shortly before chat completions without retrying schema or +model-output failures. The unchanged native report remains one final outcome per mode because the upstream report deduplicates rerun records by case and mode. It is uploaded as `kimi_vendor_report.json`, and `utils/evals/kimi_vendor_eval.py` projects those two outcomes into the existing diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 68d6f997d5..4103facc7a 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -49,9 +49,14 @@ def build_pytest_command( "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", "--reruns", - "3", + "6", "--reruns-delay", - "2", + "3", + "--only-rerun", + ( + r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" + r"APITimeoutError|Connection error|timed out)" + ), "--base-url", base_url, "--api-key", diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 3120b28dd1..c76461a59f 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -29,6 +29,7 @@ RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "minimax-provider-verifier" EXPECTED_INDICES = (71,) +M3_MODEL_REGEX = re.compile(r"(? dict[str, Any]: model=model, temperature=0, top_p=1, - max_tokens=M3_DEFAULT_MAX_TOKENS, ) + if M3_MODEL_REGEX.search(model): + request["max_tokens"] = M3_DEFAULT_MAX_TOKENS return request diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index d4dea2944b..e55babc7cc 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -1,3 +1,4 @@ +import builtins import json import subprocess import sys @@ -9,6 +10,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) import bfcl_eval as be +import validate_scores as vs def _compatibility_path(output_dir: Path) -> Path: @@ -31,6 +33,21 @@ def _score(output_dir: Path) -> float: return _compatibility(output_dir)["results"][be.TASK_NAME]["acc,none"] +def test_thresholds_are_stdlib_readable_without_pyyaml(monkeypatch) -> None: + real_import = builtins.__import__ + + def import_without_yaml(name, *args, **kwargs): + if name == "yaml": + raise ModuleNotFoundError("No module named 'yaml'") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", import_without_yaml) + thresholds = vs.load_config(str(Path(vs.__file__).with_name("thresholds.yaml"))) + + assert thresholds["default"]["bfcl_smoke"] == 0.75 + assert thresholds["default"]["bfcl_parallel"] == 0.0 + + def _write_result( project_root: Path, category: str, diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index 9e887a7018..fd2fdcdaee 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -63,9 +63,14 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", "--reruns", - "3", + "6", "--reruns-delay", - "2", + "3", + "--only-rerun", + ( + r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" + r"APITimeoutError|Connection error|timed out)" + ), "--base-url", "http://127.0.0.1:8000/v1", "--api-key", diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index bfba0ed08c..bfe1f5d552 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -206,6 +206,14 @@ def http_post(**request: Any) -> dict[str, Any]: assert "secret" not in json.dumps([native, compatibility]) +def test_non_m3_request_does_not_force_m3_token_budget() -> None: + _, rows = mpe.load_fixture(mpe.DEFAULT_FIXTURE_PATH) + + request = mpe.prepare_request(rows[0], "moonshotai/Kimi-K3") + + assert "max_tokens" not in request + + def test_schema_failure_fails_only_tool_case(tmp_path: Path) -> None: def post(payload: dict[str, Any]) -> dict[str, Any]: if payload.get("tools", [{}])[0].get("function", {}).get("name") == ( diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 69ab3d5dd2..aad5916f47 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -432,10 +432,13 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( _prepare_vendor_verifier_python() { return 12; } _prepare_minimax_vendor_runtime() { echo "UNEXPECTED_DEPENDENCY_INSTALL"; return 99; } python3() { printf 'ADAPTER_ARG=<%s>\n' "$@"; } -append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR"; } +append_lm_eval_summary() { + printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR" + printf 'STAGED_CONC=<%s>\n' "$CONC" +} export MODEL_PREFIX=minimaxm3 export MODEL=test-model -export EVAL_CONCURRENT_REQUESTS="" +export EVAL_CONCURRENT_REQUESTS=7 export EVAL_ONLY=false export IS_AGENTIC=0 run_eval --framework minimax-vendor --results-dir "$RESULTS_DIR" @@ -468,6 +471,7 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( ) in output assert f"STAGED=<{results_dir}>" in output assert output.count("STAGED=<") == 1 + assert "STAGED_CONC=<7>" in output def test_minimax_vendor_dependency_install_is_pinned_and_minimal( diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index 04c98613ad..08899f9d43 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -1,30 +1,40 @@ -# Model thresholds override task defaults. -default: - gsm8k: 0.90 - kimi_tool_call_schema: 1.0 - minimax_m3_smoke: 1.0 - # The aggregate gates at 3/4 fixed cases; per-category rows are diagnostic. - bfcl_smoke: 0.75 - bfcl_simple_python: 0.0 - bfcl_multiple: 0.0 - bfcl_parallel: 0.0 - bfcl_irrelevance: 0.0 - gpqa_diamond_cot_n_shot: 0.30 - swebench_lite: 0.50 -models: - dsr1: - gsm8k: 0.91 - dsv4: - gsm8k: 0.91 - glm5: - gsm8k: 0.94 - glm5.1: - gsm8k: 0.93 - gptoss: - gsm8k: 0.91 - kimik2.5: - gsm8k: 0.90 - minimaxm2.5: - gsm8k: 0.92 - qwen3.5: - gsm8k: 0.94 +{ + "default": { + "gsm8k": 0.90, + "kimi_tool_call_schema": 1.0, + "minimax_m3_smoke": 1.0, + "bfcl_smoke": 0.75, + "bfcl_simple_python": 0.0, + "bfcl_multiple": 0.0, + "bfcl_parallel": 0.0, + "bfcl_irrelevance": 0.0, + "gpqa_diamond_cot_n_shot": 0.30, + "swebench_lite": 0.50 + }, + "models": { + "dsr1": { + "gsm8k": 0.91 + }, + "dsv4": { + "gsm8k": 0.91 + }, + "glm5": { + "gsm8k": 0.94 + }, + "glm5.1": { + "gsm8k": 0.93 + }, + "gptoss": { + "gsm8k": 0.91 + }, + "kimik2.5": { + "gsm8k": 0.90 + }, + "minimaxm2.5": { + "gsm8k": 0.92 + }, + "qwen3.5": { + "gsm8k": 0.94 + } + } +} diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index bfcc6e5f0b..7fbf4d9a54 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -9,6 +9,7 @@ agentic_key, benchmark_key, eval_key, + eval_result_key, dedupe_reran_evals, main, validate_agentic_artifacts, @@ -417,6 +418,13 @@ def test_eval_validation_separates_explicit_suite_identities( assert validate_eval_artifacts(tmp_path) == [] +def test_eval_result_key_includes_task_identity() -> None: + gsm8k = single_eval_result(32, eval_suite="tool_use") + bfcl = {**gsm8k, "task": "bfcl_smoke"} + + assert eval_result_key(gsm8k) != eval_result_key(bfcl) + + def test_eval_validation_distinguishes_sequence_lengths(tmp_path: Path) -> None: write_eval_aggregate( tmp_path, @@ -648,6 +656,51 @@ def test_eval_validation_accepts_neutral_result_format_marker( assert validate_eval_artifacts(tmp_path) == [] +def test_eval_validation_accepts_neutral_filter_primary_score( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32, eval_suite="bfcl_smoke") + result_path = next(tmp_path.glob("eval_*/results*.json")) + data = json.loads(result_path.read_text()) + data["results"]["gsm8k"] = { + "acc,none": 1.0, + "acc_stderr,none": 0.0, + } + data["configs"]["gsm8k"] = { + "metric_list": [{"metric": "acc"}], + "filter_list": [{"name": "none"}], + } + result_path.write_text(json.dumps(data)) + write_eval_aggregate( + tmp_path, + [single_eval_result(32, eval_suite="bfcl_smoke")], + ) + + assert validate_eval_artifacts(tmp_path) == [] + + +def test_eval_validation_accepts_multiple_tasks_from_one_artifact( + tmp_path: Path, +) -> None: + write_raw_eval_artifact(tmp_path, 32, eval_suite="tool_use") + result_path = next(tmp_path.glob("eval_*/results*.json")) + data = json.loads(result_path.read_text()) + data["results"]["bfcl_smoke"] = { + "exact_match,strict-match": 1.0, + "exact_match_stderr,strict-match": 0.0, + } + data["configs"]["bfcl_smoke"] = data["configs"]["gsm8k"] + data["n-samples"]["bfcl_smoke"] = {"effective": 1} + result_path.write_text(json.dumps(data)) + base_row = single_eval_result(32, eval_suite="tool_use") + write_eval_aggregate( + tmp_path, + [base_row, {**base_row, "task": "bfcl_smoke"}], + ) + + assert validate_eval_artifacts(tmp_path) == [] + + def test_eval_validation_rejects_invalid_legacy_concurrency( tmp_path: Path, ) -> None: @@ -911,9 +964,9 @@ def test_dedupe_keeps_latest_legacy_rerun(tmp_path: Path) -> None: assert any("kept 1 of 3" in message for message in messages) -def test_dedupe_leaves_ambiguous_duplicates_for_validation(tmp_path: Path) -> None: - # Duplicate raw identities with no result timestamps cannot be ordered, so - # dedupe must leave them and validation must still reject them. +def test_dedupe_leaves_ambiguous_artifacts_for_validation(tmp_path: Path) -> None: + # Result-less raw artifacts cannot be ordered or reused. Dedupe leaves them + # for validation to reject as missing recognized results. for name in ("eval_minimaxm3_conc4096_b300-nv_01", "eval_minimaxm3_conc4096_b300-nv_02"): _dd_write_legacy_raw(tmp_path, name, 4096, None) _dd_write_aggregate( @@ -922,7 +975,10 @@ def test_dedupe_leaves_ambiguous_duplicates_for_validation(tmp_path: Path) -> No ) assert dedupe_reran_evals(tmp_path) == [] - assert any("duplicate" in e for e in validate_eval_artifacts(tmp_path)) + assert any( + "no recognized eval result" in error + for error in validate_eval_artifacts(tmp_path) + ) def test_dedupe_is_noop_for_clean_artifacts(tmp_path: Path) -> None: @@ -1276,7 +1332,12 @@ def test_eval_validation_accepts_extract_filter_primary_score( raw_path.write_text(json.dumps(data)) write_eval_aggregate( tmp_path, - [single_eval_result(32, eval_suite="gpqa")], + [ + { + **single_eval_result(32, eval_suite="gpqa"), + "task": "gpqa", + } + ], ) assert validate_eval_artifacts(tmp_path) == [] @@ -1333,4 +1394,4 @@ def test_dedupe_uses_winning_legacy_result_mtime_for_aggregate( (tmp_path / "eval_results_all" / "agg_eval_all.json").read_text() ) assert [row["em_strict"] for row in rows] == [0.9] - assert validate_eval_artifacts(tmp_path) == [] \ No newline at end of file + assert validate_eval_artifacts(tmp_path) == [] diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 4626ace73a..9c50bf8240 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -376,6 +376,11 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: ) +def eval_result_key(row: dict[str, Any]) -> tuple[Any, ...]: + """Build a task-level eval result identity.""" + return (*eval_key(row), row.get("task")) + + def raw_eval_artifact_dirs(artifacts_dir: Path) -> list[Path]: """Return raw eval result artifacts, excluding aggregate and debug artifacts.""" return sorted( @@ -499,7 +504,6 @@ def raw_eval_key_rows( errors.extend(meta_errors) if meta_errors: continue - rows.extend(key for key, _ in contributions) result_paths = _recognized_eval_result_paths( artifact_dir.glob("results*.json") @@ -539,6 +543,16 @@ def raw_eval_key_rows( f"raw eval artifact {artifact_dir.name!r} latest result " f"{latest.name!r}{conc_label} {result_error}" ) + continue + result_data = load_json(latest) + result_tasks = result_data["results"] + contribution_meta = ( + {**meta, "conc": conc} if conc is not None else meta + ) + rows.extend( + eval_result_key({**contribution_meta, "task": task}) + for task in result_tasks + ) return rows, errors @@ -584,7 +598,7 @@ def validate_eval_artifacts( "has invalid eval_suite" ) continue - aggregate_rows.append(eval_key(row)) + aggregate_rows.append(eval_result_key(row)) if row_count == 0: errors.append("eval_results_all contains no rows") errors.extend( @@ -755,16 +769,21 @@ def _raw_result_error(path: Path) -> Optional[str]: for item in filter_list ): return f"has malformed filter config for task {task!r}" - strict_names = [ + configured_names = [ f"{base_metric},{item['name']}" for item in filter_list - if "strict" in item["name"] or "resolved" in item["name"] ] - primary_names = strict_names or [ - f"{base_metric},{item['name']}" - for item in filter_list - if "flex" in item["name"] or "extract" in item["name"] + strict_names = [ + name + for name in configured_names + if "strict" in name or "resolved" in name ] + fallback_names = [ + name + for name in configured_names + if "flex" in name or "extract" in name + ] + primary_names = strict_names or fallback_names or configured_names else: primary_names = ["acc" if "acc" in metrics else base_metric] if not primary_names or any(name not in metrics for name in primary_names): @@ -902,7 +921,7 @@ def _dedupe_eval_aggregate( loaded[agg_path] = data for index, row in enumerate(data): if isinstance(row, dict) and not invalid_eval_suite(row): - groups.setdefault(eval_key(row), []).append( + groups.setdefault(eval_result_key(row), []).append( (agg_path, index, row) ) @@ -930,7 +949,8 @@ def _dedupe_eval_aggregate( winner_result_names[key] = max(candidates, key=_result_order).name for key, entries in groups.items(): - winner = winners.get(key) + artifact_key = key[:-1] + winner = winners.get(artifact_key) if winner is None or len(entries) == 1: continue matching = [ @@ -938,7 +958,7 @@ def _dedupe_eval_aggregate( for entry in entries if _source_names_raw_dir(entry[2].get("source"), winner) ] - winner_result_name = winner_result_names.get(key) + winner_result_name = winner_result_names.get(artifact_key) exact_matching = [ entry for entry in matching From 592215f6770b8ce7362aeda56cbf94d153a743a6 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:43:21 -0500 Subject: [PATCH 51/99] feat: add opt-in full verifier suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增可选的 Kimi 与 MiniMax 全量验证套件,用于隔离式诊断实验,并保持现有 smoke 默认行为不变。 --- .../workflows/benchmark-multinode-tmpl.yml | 2 + .github/workflows/benchmark-tmpl.yml | 2 + benchmarks/benchmark_lib.sh | 142 ++++- utils/evals/kimi_vendor_eval.py | 205 ++++++- utils/evals/minimax_m3_full_eval.py | 526 ++++++++++++++++++ utils/evals/test_kimi_vendor_eval.py | 156 +++++- utils/evals/test_minimax_m3_full_eval.py | 200 +++++++ utils/evals/test_run_eval_dispatch.py | 144 +++++ utils/evals/thresholds.yaml | 2 + 9 files changed, 1334 insertions(+), 45 deletions(-) create mode 100755 utils/evals/minimax_m3_full_eval.py create mode 100644 utils/evals/test_minimax_m3_full_eval.py diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 68d0bee0d1..48fd1951fc 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -493,6 +493,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + *_vendor_results.jsonl sample*.jsonl agent_preds.json predictions.jsonl @@ -520,6 +521,7 @@ jobs: rm -f meta_env.json || true rm -f results*.json || true rm -f -- ./*_vendor_report.json || true + rm -f -- ./*_vendor_results.jsonl || true rm -f bfcl_report.json || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index a3ed000923..6d41a2002d 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -414,6 +414,7 @@ jobs: meta_env.json results*.json *_vendor_report.json + *_vendor_results.jsonl bfcl_report.json sample*.jsonl agent_preds.json @@ -438,6 +439,7 @@ jobs: # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true rm -f -- ./*_vendor_report.json || true + rm -f -- ./*_vendor_results.jsonl || true rm -f sample*.jsonl || true rm -f bfcl_report.json || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 0d850cc53d..ff61d10579 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -962,18 +962,26 @@ _prepare_vendor_verifier_python() { _install_kimi_vendor_eval_deps() { local target_dir="$1" - "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ - "httpx[http2]==0.28.1" \ - "openai==2.14.0" \ - "jsonschema==4.25.1" \ - "pytest==8.4.2" \ + local eval_suite="${2:-kimi_tool_call_schema}" + local -a packages=( + "httpx[http2]==0.28.1" + "openai==2.14.0" + "jsonschema==4.25.1" + "pytest==8.4.2" "pytest-rerunfailures==16.4" + ) + if [ "$eval_suite" = "kimi_tool_call_schema_full" ]; then + packages+=("pytest-xdist==3.8.0") + fi + "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir \ + --target "$target_dir" "${packages[@]}" } _prepare_kimi_vendor_runtime() { + local eval_suite="${1:-kimi_tool_call_schema}" local runtime_dir install_rc=0 runtime_dir="$(mktemp -d /tmp/kimi-vendor-runtime-XXXXXX)" || return $? - _install_kimi_vendor_eval_deps "$runtime_dir" >&2 || install_rc=$? + _install_kimi_vendor_eval_deps "$runtime_dir" "$eval_suite" >&2 || install_rc=$? if [ "$install_rc" -ne 0 ]; then rm -rf "$runtime_dir" return "$install_rc" @@ -1233,11 +1241,13 @@ _write_kimi_vendor_integration_error() { local adapter_path="$1" local model_name="$2" local results_dir="$3" - local message="$4" + local task_name="$4" + local message="$5" python3 "$adapter_path" \ --model "$model_name" \ --output-dir "$results_dir" \ + --task-name "$task_name" \ --integration-error "$message" } @@ -1246,6 +1256,11 @@ _run_kimi_tool_call_schema_eval() { local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}" local verifier_repo="https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" local verifier_ref="b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" + local timeout_seconds=900 + if [ "$eval_suite" = "kimi_tool_call_schema_full" ]; then + timeout_seconds=7200 + fi while [[ $# -gt 0 ]]; do case "$1" in @@ -1283,7 +1298,7 @@ _run_kimi_tool_call_schema_eval() { integration_error="Kimi Vendor Verifier Python runtime preparation failed with exit code ${setup_rc}" } if [ "$setup_rc" -eq 0 ]; then - runtime_dir=$(_prepare_kimi_vendor_runtime) || { + runtime_dir=$(_prepare_kimi_vendor_runtime "$eval_suite") || { setup_rc=$? integration_error="Kimi Vendor Verifier dependency installation failed with exit code ${setup_rc}" } @@ -1302,8 +1317,8 @@ _run_kimi_tool_call_schema_eval() { echo "ERROR: ${integration_error}" >&2 local artifact_rc=0 _write_kimi_vendor_integration_error \ - "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ - || artifact_rc=$? + "$adapter_path" "$model_name" "$results_dir" "$eval_suite" \ + "$integration_error" || artifact_rc=$? if [ "$artifact_rc" -ne 0 ]; then echo "ERROR: failed to write Kimi verifier failure artifact (exit code ${artifact_rc})" >&2 fi @@ -1319,6 +1334,8 @@ _run_kimi_tool_call_schema_eval() { --model "$model_name" \ --model-prefix "${MODEL_PREFIX:-}" \ --output-dir "$results_dir" \ + --task-name "$eval_suite" \ + --timeout-seconds "$timeout_seconds" \ || eval_rc=$? _cleanup_vendor_eval \ "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" @@ -1330,7 +1347,7 @@ run_kimi_vendor_eval() { export EVAL_SUITE="$eval_suite" case "$eval_suite" in - kimi_tool_call_schema) + kimi_tool_call_schema|kimi_tool_call_schema_full) _run_kimi_tool_call_schema_eval "$@" ;; *) @@ -1645,6 +1662,106 @@ _run_minimax_m3_smoke_eval() { return "$eval_rc" } +_install_minimax_m3_full_deps() { + local target_dir="$1" + "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ + "jsonschema==4.25.1" \ + "loguru==0.7.3" \ + "megfile==4.2.5" \ + "numpy==2.3.4" \ + "openai==2.7.1" \ + "tqdm==4.67.1" +} + +_prepare_minimax_m3_full_runtime() { + local adapter_path="$1" + local runtime_dir prepare_rc=0 + runtime_dir="$(mktemp -d /tmp/minimax-m3-full-runtime-XXXXXX)" || return $? + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" prepare-source \ + --source-dir "${runtime_dir}/source" >&2 || prepare_rc=$? + if [ "$prepare_rc" -eq 0 ]; then + _install_minimax_m3_full_deps "${runtime_dir}/deps" >&2 || prepare_rc=$? + fi + if [ "$prepare_rc" -ne 0 ]; then + rm -rf "$runtime_dir" + return "$prepare_rc" + fi + printf '%s\n' "$runtime_dir" +} + +_run_minimax_m3_full_eval() { + local port="${PORT:-8888}" + local results_dir="${EVAL_RESULT_DIR:-}" + + while [[ $# -gt 0 ]]; do + case "$1" in + --port|--results-dir) + if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then + echo "ERROR: $1 requires a value" >&2 + return 2 + fi + case "$1" in + --port) port="$2" ;; + --results-dir) results_dir="$2" ;; + esac + shift 2 + ;; + *) + echo "Unknown parameter: $1" >&2 + return 2 + ;; + esac + done + + if [ -z "$results_dir" ]; then + results_dir="$(mktemp -d /tmp/eval_out-XXXXXX)" || return $? + fi + + local model_name="${MODEL_NAME:-${MODEL:-}}" + local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/minimax_m3_full_eval.py" + local runtime_dir="" + + mkdir -p "$results_dir" || return $? + results_dir="$(cd "$results_dir" && pwd)" || return $? + export EVAL_RESULT_DIR="$results_dir" + + local setup_rc=0 integration_error="" + _prepare_vendor_verifier_python "MiniMax M3 full verifier" "minimax-m3-full-python" || { + setup_rc=$? + integration_error="MiniMax M3 full Python runtime preparation failed with exit code ${setup_rc}" + } + if [ "$setup_rc" -eq 0 ]; then + runtime_dir=$(_prepare_minimax_m3_full_runtime "$adapter_path") || { + setup_rc=$? + integration_error="MiniMax M3 full pinned runtime preparation failed with exit code ${setup_rc}" + } + fi + if [ "$setup_rc" -ne 0 ]; then + echo "ERROR: ${integration_error}" >&2 + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" failure \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --message "$integration_error" || true + _cleanup_vendor_eval \ + "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + return "$setup_rc" + fi + + local eval_rc=0 + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" run \ + --python "${VENDOR_VERIFIER_PYTHON:-python3}" \ + --source-dir "${runtime_dir}/source" \ + --dependency-dir "${runtime_dir}/deps" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --model "$model_name" \ + --output-dir "$results_dir" \ + || eval_rc=$? + _cleanup_vendor_eval \ + "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" + return "$eval_rc" +} + + run_minimax_vendor_eval() { local eval_suite="${EVAL_SUITE:-minimax_m3_smoke}" export EVAL_SUITE="$eval_suite" @@ -1653,6 +1770,9 @@ run_minimax_vendor_eval() { minimax_m3_smoke) _run_minimax_m3_smoke_eval "$@" ;; + minimax_m3_full) + _run_minimax_m3_full_eval "$@" + ;; *) echo "ERROR: unsupported MiniMax Provider Verifier suite '${eval_suite}'" >&2 export EVAL_RESULT_DIR="" diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 4103facc7a..3845a61e05 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -13,10 +13,16 @@ from typing import Any TASK_NAME = "kimi_tool_call_schema" +FULL_TASK_NAME = "kimi_tool_call_schema_full" +SUPPORTED_TASK_NAMES = (TASK_NAME, FULL_TASK_NAME) NATIVE_REPORT_FILENAME = "kimi_vendor_report.json" COMPATIBILITY_GLOB = "results_kimi_vendor_*.json" EXPECTED_MODES = {"non-stream", "stream"} +EXPECTED_TOTALS = {TASK_NAME: 2, FULL_TASK_NAME: 408} +FULL_SELECTED_CASES = 204 DEFAULT_TIMEOUT_SECONDS = 900 +FULL_TIMEOUT_SECONDS = 7200 +FULL_WORKERS = 8 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" @@ -36,18 +42,27 @@ def build_pytest_command( model: str, model_prefix: str = "", report_path: Path, + task_name: str = TASK_NAME, ) -> list[str]: - """Build the fixed Phase 1 invocation of the upstream verifier.""" + """Build the fixed invocation of the pinned upstream verifier.""" + _expected_total(task_name) thinking_args = ( ["--think-mode", "opensource", "--thinking"] if model_prefix == "dsv4" else ["--think-mode", "none"] ) + parallel_args = ["-n", str(FULL_WORKERS)] if task_name == FULL_TASK_NAME else [] + selection_args = ( + ["--selection", "all"] + if task_name == FULL_TASK_NAME + else ["--selection", "object", "--max-cases", "1"] + ) return [ sys.executable, "-m", "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", + *parallel_args, "--reruns", "6", "--reruns-delay", @@ -64,10 +79,7 @@ def build_pytest_command( "--smoke-model", model, *thinking_args, - "--selection", - "object", - "--max-cases", - "1", + *selection_args, "--case-dir", "testdata/walle_validator_cases/validator_cases", "--max-tokens", @@ -82,8 +94,20 @@ def _mapping(value: Any, name: str) -> Mapping[str, Any]: raise ValueError(f"{name} must be an object") return value +def _expected_total(task_name: str) -> int: + try: + return EXPECTED_TOTALS[task_name] + except KeyError as exc: + raise ValueError(f"unsupported Kimi task: {task_name}") from exc + -def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: +def _project_report( + model: str, + report: Any, + *, + task_name: str = TASK_NAME, +) -> tuple[dict[str, Any], bool]: + expected_total = _expected_total(task_name) root = _mapping(report, "report") summary = _mapping(root.get("summary"), "report.summary") results = root.get("results") @@ -105,8 +129,45 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: if sum(by_status.values()) != total: raise ValueError("report summary does not match total") passed = by_status.get("passed", 0) + selected_identities: set[tuple[str, int]] = set() + if task_name == FULL_TASK_NAME: + selected_cases = root.get("selected_cases") + if ( + not isinstance(selected_cases, list) + or len(selected_cases) != FULL_SELECTED_CASES + ): + raise ValueError( + f"report.selected_cases must contain {FULL_SELECTED_CASES} cases" + ) + selected_keys: set[tuple[str, int, str]] = set() + for index, selected_case in enumerate(selected_cases): + record = _mapping( + selected_case, + f"report.selected_cases[{index}]", + ) + suite = record.get("suite") + line = record.get("line") + selection_reason = record.get("selection_reason") + if ( + not isinstance(suite, str) + or not suite + or not isinstance(line, int) + or isinstance(line, bool) + or line < 1 + or not isinstance(selection_reason, str) + ): + raise ValueError( + f"report.selected_cases[{index}] has invalid identity" + ) + selected_key = (suite, line, selection_reason) + if selected_key in selected_keys: + raise ValueError("report.selected_cases contains a duplicate case") + selected_keys.add(selected_key) + selected_identities.add((suite, line)) + modes: list[str] = [] + case_modes: dict[tuple[str, int], set[str]] = {} result_passes = 0 for index, result in enumerate(results): record = _mapping(result, f"report.results[{index}]") @@ -121,46 +182,87 @@ def _project_report(model: str, report: Any) -> tuple[dict[str, Any], bool]: modes.append(mode) result_passes += status == "passed" + if task_name == FULL_TASK_NAME: + suite = record.get("suite") + line = record.get("line") + if ( + not isinstance(suite, str) + or not suite + or not isinstance(line, int) + or isinstance(line, bool) + or line < 1 + ): + raise ValueError( + f"report.results[{index}] has invalid suite or line identity" + ) + identity = (suite, line) + identity_modes = case_modes.setdefault(identity, set()) + if mode in identity_modes: + raise ValueError( + "report contains a duplicate mode for a selected suite and line" + ) + identity_modes.add(mode) + if total != len(results) or passed != result_passes: raise ValueError("report summary does not match result records") + if total != expected_total: + raise ValueError( + f"report contains {total} records; expected {expected_total} for {task_name}" + ) - if ( - total != 2 - or len(results) != 2 - or set(modes) != EXPECTED_MODES - or len(modes) != len(set(modes)) - ): - raise ValueError("report does not contain the expected stream modes") - score = passed / 2.0 - return _compatibility_result(model, score, n_samples=2), passed == 2 + if task_name == TASK_NAME: + if set(modes) != EXPECTED_MODES or len(modes) != len(set(modes)): + raise ValueError("report does not contain the expected stream modes") + elif any(modes_for_case != EXPECTED_MODES for modes_for_case in case_modes.values()): + raise ValueError( + "report does not contain exactly one of each stream mode " + "for every selected suite and line" + ) + if task_name == FULL_TASK_NAME and set(case_modes) != selected_identities: + raise ValueError( + "report results do not match the selected suite and line identities" + ) + + score = passed / total + return ( + _compatibility_result( + model, + score, + task_name=task_name, + n_samples=total, + ), + passed == total, + ) def _compatibility_result( model: str, score: float, *, + task_name: str = TASK_NAME, n_samples: int, integration_error: BaseException | None = None, ) -> dict[str, Any]: + expected_total = _expected_total(task_name) result: dict[str, Any] = { "result_format": RESULT_FORMAT, "eval_adapter": ADAPTER_NAME, "model_name": model, "results": { - TASK_NAME: { + task_name: { "exact_match,strict-match": score, "exact_match_stderr,strict-match": 0.0, } }, "configs": { - TASK_NAME: { + task_name: { "metric_list": [{"metric": "exact_match"}], "filter_list": [{"name": "strict-match"}], } }, "n-samples": { - TASK_NAME: { - "original": len(EXPECTED_MODES), + task_name: { + "original": expected_total, "effective": n_samples, } }, @@ -185,16 +287,24 @@ def run_evaluation( model: str, model_prefix: str = "", output_dir: Path, + task_name: str = TASK_NAME, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" + expected_total = _expected_total(task_name) output_dir.mkdir(parents=True, exist_ok=True) native_report = output_dir / NATIVE_REPORT_FILENAME compatibility_path = prepare_compatibility_path(output_dir) subprocess_rc: int | None = None integration_error: BaseException | None = None - compatibility = _compatibility_result(model, 0.0, n_samples=0) - complete_pass = False + compatibility = _compatibility_result( + model, + 0.0, + task_name=task_name, + n_samples=0, + ) + all_passed = False + completed_successfully = False try: native_report.unlink(missing_ok=True) completed = subprocess.run( @@ -204,6 +314,7 @@ def run_evaluation( model=model, model_prefix=model_prefix, report_path=native_report.resolve(), + task_name=task_name, ), cwd=verifier_dir, check=False, @@ -211,19 +322,39 @@ def run_evaluation( ) subprocess_rc = completed.returncode report = json.loads(native_report.read_text(encoding="utf-8")) - compatibility, complete_pass = _project_report(model, report) - if subprocess_rc != 0 and complete_pass: + compatibility, all_passed = _project_report( + model, + report, + task_name=task_name, + ) + if task_name == FULL_TASK_NAME: + completed_successfully = subprocess_rc == 0 or ( + subprocess_rc == 1 and not all_passed + ) + invalid_exit = not completed_successfully + else: + completed_successfully = subprocess_rc == 0 and all_passed + invalid_exit = subprocess_rc != 0 and all_passed + if invalid_exit: integration_error = RuntimeError( f"upstream verifier exited with code {subprocess_rc}" ) compatibility = _compatibility_result( - model, 0.0, n_samples=2, integration_error=integration_error + model, + 0.0, + task_name=task_name, + n_samples=expected_total, + integration_error=integration_error, ) - complete_pass = False + completed_successfully = False except (OSError, ValueError, subprocess.TimeoutExpired) as exc: integration_error = exc compatibility = _compatibility_result( - model, 0.0, n_samples=0, integration_error=exc + model, + 0.0, + task_name=task_name, + n_samples=0, + integration_error=exc, ) finally: try: @@ -233,7 +364,7 @@ def run_evaluation( exc.add_note(f"Earlier integration error: {integration_error}") raise - return subprocess_rc == 0 and complete_pass and integration_error is None + return completed_successfully and integration_error is None def _positive_int(value: str) -> int: @@ -246,7 +377,7 @@ def _positive_int(value: str) -> int: def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Run the pinned stock Kimi Vendor Verifier tool-schema smoke test." + description="Run the pinned stock Kimi Vendor Verifier tool-schema evaluation." ) parser.add_argument("--verifier-dir", type=Path) parser.add_argument("--base-url") @@ -255,8 +386,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--model-prefix", default="") parser.add_argument("--output-dir", required=True, type=Path) parser.add_argument( - "--timeout-seconds", type=_positive_int, default=DEFAULT_TIMEOUT_SECONDS + "--task-name", + choices=SUPPORTED_TASK_NAMES, + default=TASK_NAME, ) + parser.add_argument("--timeout-seconds", type=_positive_int) parser.add_argument("--integration-error") args = parser.parse_args(argv) if args.integration_error is None: @@ -285,11 +419,21 @@ def main(argv: Sequence[str] | None = None) -> int: _compatibility_result( args.model, 0.0, + task_name=args.task_name, n_samples=0, integration_error=RuntimeError(args.integration_error), ), ) return 0 + timeout_seconds = ( + args.timeout_seconds + if args.timeout_seconds is not None + else ( + FULL_TIMEOUT_SECONDS + if args.task_name == FULL_TASK_NAME + else DEFAULT_TIMEOUT_SECONDS + ) + ) passed = run_evaluation( verifier_dir=args.verifier_dir, base_url=args.base_url, @@ -297,7 +441,8 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, model_prefix=args.model_prefix, output_dir=args.output_dir, - timeout_seconds=args.timeout_seconds, + task_name=args.task_name, + timeout_seconds=timeout_seconds, ) return 0 if passed else 1 diff --git a/utils/evals/minimax_m3_full_eval.py b/utils/evals/minimax_m3_full_eval.py new file mode 100755 index 0000000000..af8d1fa2cd --- /dev/null +++ b/utils/evals/minimax_m3_full_eval.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +"""Run and project the pinned full MiniMax M3 provider verifier.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import subprocess +import urllib.error +import urllib.request +from collections.abc import Callable, Mapping, Sequence +from datetime import datetime, timezone +from pathlib import Path, PurePosixPath +from typing import Any + +TASK_NAME = "minimax_m3_full" +RESULT_FORMAT = "inferencex-eval-v1" +ADAPTER_NAME = "minimax-provider-verifier" +NATIVE_REPORT_FILENAME = "minimax_vendor_report.json" +NATIVE_RESULTS_FILENAME = "minimax_vendor_results.jsonl" +COMPATIBILITY_GLOB = "results_minimax_vendor_full_*.json" +EXPECTED_RESULT_COUNT = 102 +UPSTREAM_REF = "85bf180e54e2ab0b31595cfdc697116c4760876d" +UPSTREAM_BASE_URL = ( + "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/" + f"{UPSTREAM_REF}" +) +EXPECTED_SAMPLE_SHA256 = ( + "3ead102af0f888acc95867b3a9916942524b02f4f64931f020a1bfb4fee9aae2" +) +REQUIRED_SOURCE_SHA256 = { + "verify.py": "6bc00948d9be06189f31c5a53bb7929b15555402f0b3495609d26b468090ee4a", + "sample.jsonl": EXPECTED_SAMPLE_SHA256, + "validator/__init__.py": "955a5ee77b72fb1d128f5d1ab6c65072e54b8cf527916e70a69e251811087e7b", + "validator/base.py": "00f1776d4b4d4200e4ce865f044ef9cdd7f375ca3b752a8eefba4ab375f7e03d", + "validator/tool_calls.py": "eb6a91a704a3e1706a1fc0f0f4233f4dd08ecb38b7f41fd19abaf5e981d0406b", + "validator/russian_characters.py": "09b429f45b43b34c241d5b54401aab6c4c55de228d87c882bb7f8858c9856c0c", + "validator/repeat_ngram.py": "72948cd9501e8daeb49a2c9fac4421a182191fc4251846713021c27d1a5fa315", + "validator/scenario_check.py": "51d691a1595f6fa3193f4f624a98581a9795323e77439df7a2fd5d83950c5ba1", +} +MAX_SOURCE_BYTES = 16 * 1024 * 1024 +DOWNLOAD_TIMEOUT_SECONDS = 60 +UPSTREAM_TIMEOUT_SECONDS = 12 * 60 * 60 + +Runner = Callable[..., subprocess.CompletedProcess[Any]] +Fetcher = Callable[[str], bytes] + + +class FullSuiteError(RuntimeError): + """The full verifier could not produce one complete diagnostic run.""" + + +class _RejectRedirects(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *args: Any, **kwargs: Any) -> None: + return None + + +_NO_REDIRECT_OPENER = urllib.request.build_opener(_RejectRedirects()) + + +def source_url(relative_path: str) -> str: + """Return the immutable URL for one allowlisted required source file.""" + if relative_path not in REQUIRED_SOURCE_SHA256: + raise ValueError(f"unapproved upstream source path: {relative_path!r}") + path = PurePosixPath(relative_path) + if path.is_absolute() or ".." in path.parts: + raise ValueError(f"unsafe upstream source path: {relative_path!r}") + return f"{UPSTREAM_BASE_URL}/{relative_path}" + + +def verify_source_content(relative_path: str, content: bytes) -> None: + """Verify one allowlisted file against its pinned SHA256 identity.""" + if relative_path not in REQUIRED_SOURCE_SHA256: + raise ValueError(f"unapproved upstream source path: {relative_path!r}") + if not isinstance(content, bytes): + raise TypeError("source content must be bytes") + if len(content) > MAX_SOURCE_BYTES: + raise ValueError(f"pinned source {relative_path} exceeds the size limit") + actual = hashlib.sha256(content).hexdigest() + expected = REQUIRED_SOURCE_SHA256[relative_path] + if actual != expected: + raise ValueError( + f"pinned source {relative_path} SHA256 mismatch: " + f"expected {expected}, got {actual}" + ) + + +def _fetch_source(relative_path: str) -> bytes: + url = source_url(relative_path) + request = urllib.request.Request( + url, + headers={"Accept": "application/octet-stream"}, + method="GET", + ) + try: + with _NO_REDIRECT_OPENER.open( + request, timeout=DOWNLOAD_TIMEOUT_SECONDS + ) as response: + status = getattr(response, "status", None) + if status != 200 or response.geturl() != url: + raise FullSuiteError( + f"unexpected response for pinned source {relative_path}: " + f"status={status!r}, url={response.geturl()!r}" + ) + declared_size = response.headers.get("Content-Length") + if declared_size is not None and int(declared_size) > MAX_SOURCE_BYTES: + raise FullSuiteError( + f"pinned source {relative_path} exceeds the size limit" + ) + content = response.read(MAX_SOURCE_BYTES + 1) + except (OSError, ValueError, urllib.error.URLError) as exc: + raise FullSuiteError( + f"failed to download pinned source {relative_path}: {exc}" + ) from exc + verify_source_content(relative_path, content) + return content + + +def _validate_sample(content: bytes) -> None: + try: + text = content.decode("utf-8") + except UnicodeDecodeError as exc: + raise ValueError("pinned sample.jsonl is not UTF-8") from exc + lines = text.splitlines() + if len(lines) != EXPECTED_RESULT_COUNT or any(not line.strip() for line in lines): + raise ValueError( + f"pinned sample.jsonl must contain exactly {EXPECTED_RESULT_COUNT} rows" + ) + for line_number, line in enumerate(lines, 1): + try: + row = json.loads(line) + except json.JSONDecodeError as exc: + raise ValueError( + f"pinned sample.jsonl row {line_number} is invalid JSON" + ) from exc + if not isinstance(row, dict): + raise ValueError( + f"pinned sample.jsonl row {line_number} must be an object" + ) + + +def verify_source_tree(source_dir: Path) -> None: + """Verify every required file in an already prepared source directory.""" + root = source_dir.resolve() + for relative_path in REQUIRED_SOURCE_SHA256: + path = source_dir / relative_path + if path.is_symlink() or not path.is_file(): + raise ValueError(f"required pinned source is missing: {relative_path}") + if not path.resolve().is_relative_to(root): + raise ValueError(f"required pinned source escapes runtime: {relative_path}") + content = path.read_bytes() + verify_source_content(relative_path, content) + if relative_path == "sample.jsonl": + _validate_sample(content) + + +def prepare_source_tree(source_dir: Path, fetcher: Fetcher = _fetch_source) -> None: + """Download only the allowlisted verifier files and verify the complete tree.""" + source_dir.mkdir(parents=True, exist_ok=False) + for relative_path in REQUIRED_SOURCE_SHA256: + content = fetcher(relative_path) + verify_source_content(relative_path, content) + destination = source_dir / relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = destination.with_name(f".{destination.name}.tmp") + temporary.write_bytes(content) + temporary.replace(destination) + verify_source_tree(source_dir) + + +def build_verifier_command( + *, + python: Path, + source_dir: Path, + base_url: str, + model: str, + output_dir: Path, +) -> list[str]: + """Build the single pinned-upstream invocation for all 102 rows.""" + if not model.strip(): + raise ValueError("model must be a non-empty string") + if not base_url.startswith(("http://", "https://")): + raise ValueError("base_url must be an absolute HTTP(S) URL") + extra_body = json.dumps( + {"temperature": 0, "top_p": 1, "max_tokens": 40960}, + separators=(",", ":"), + ) + return [ + str(python), + str(source_dir / "verify.py"), + str(source_dir / "sample.jsonl"), + "--model", + model, + "--base-url", + base_url.rstrip("/"), + "--api-key", + "EMPTY", + "--concurrency", + "5", + "--output", + str(output_dir / NATIVE_RESULTS_FILENAME), + "--summary", + str(output_dir / NATIVE_REPORT_FILENAME), + "--timeout", + "600", + "--retries", + "3", + "--extra-body", + extra_body, + ] + + +def _error_dict(error: BaseException) -> dict[str, str]: + return {"type": type(error).__name__, "message": str(error)} + + +def _compatibility_result( + *, + model: str, + score: float, + effective: int, + integration_error: BaseException | None = None, +) -> dict[str, Any]: + result: dict[str, Any] = { + "result_format": RESULT_FORMAT, + "eval_adapter": ADAPTER_NAME, + "model_name": model, + "results": { + TASK_NAME: { + "exact_match,strict-match": score, + "exact_match_stderr,strict-match": 0.0, + } + }, + "configs": { + TASK_NAME: { + "metric_list": [{"metric": "exact_match"}], + "filter_list": [{"name": "strict-match"}], + "native_metric": "tool_calls_match_rate", + "diagnostic_threshold": 0.0, + } + }, + "n-samples": { + TASK_NAME: { + "original": EXPECTED_RESULT_COUNT, + "effective": effective, + } + }, + "source": { + "repository": "MiniMax-AI/MiniMax-Provider-Verifier", + "ref": UPSTREAM_REF, + "sample_sha256": EXPECTED_SAMPLE_SHA256, + }, + } + if integration_error is not None: + result["integration_error"] = _error_dict(integration_error) + return result + + +def _compatibility_path(output_dir: Path) -> Path: + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") + return output_dir / f"results_minimax_vendor_full_{timestamp}.json" + + +def _write_json(path: Path, value: Mapping[str, Any]) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def _read_native_report(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise FullSuiteError(f"native summary is unavailable or invalid: {exc}") from exc + if not isinstance(value, dict): + raise FullSuiteError("native summary must be a JSON object") + return value + + +def _read_native_results(path: Path) -> list[dict[str, Any]]: + try: + lines = path.read_text(encoding="utf-8").splitlines() + except OSError as exc: + raise FullSuiteError(f"native results are unavailable: {exc}") from exc + if len(lines) != EXPECTED_RESULT_COUNT or any(not line.strip() for line in lines): + raise FullSuiteError( + f"native results must contain exactly {EXPECTED_RESULT_COUNT} rows, " + f"found {len(lines)}" + ) + results: list[dict[str, Any]] = [] + for line_number, line in enumerate(lines, 1): + try: + value = json.loads(line) + except json.JSONDecodeError as exc: + raise FullSuiteError( + f"native result row {line_number} is invalid JSON" + ) from exc + if not isinstance(value, dict): + raise FullSuiteError( + f"native result row {line_number} must be a JSON object" + ) + results.append(value) + return results + + +def _count(value: Any, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise FullSuiteError(f"native summary {name} must be a non-negative integer") + return value + + +def _rate(value: Any, name: str) -> float: + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + or not 0.0 <= value <= 1.0 + ): + raise FullSuiteError(f"native summary {name} must be a finite rate") + return float(value) + + +def project_native_artifacts(*, output_dir: Path, model: str) -> Path: + """Validate a complete native run and emit its compatibility projection.""" + report = _read_native_report(output_dir / NATIVE_REPORT_FILENAME) + results = _read_native_results(output_dir / NATIVE_RESULTS_FILENAME) + indices = [row.get("data_index") for row in results] + if indices != list(range(1, EXPECTED_RESULT_COUNT + 1)): + raise FullSuiteError("native results must retain ordered data_index values 1..102") + failed_indices = [ + row["data_index"] for row in results if row.get("status") != "success" + ] + if failed_indices: + raise FullSuiteError( + "native verifier has transport failures at data_index " + + ", ".join(str(index) for index in failed_indices) + ) + success_count = _count(report.get("success_count"), "success_count") + failure_count = _count(report.get("failure_count"), "failure_count") + if success_count != EXPECTED_RESULT_COUNT or failure_count != 0: + raise FullSuiteError( + "native summary is incomplete: " + f"success_count={success_count}, failure_count={failure_count}" + ) + if report.get("model") != model: + raise FullSuiteError("native summary model does not match the requested model") + score = _rate(report.get("tool_calls_match_rate"), "tool_calls_match_rate") + compatibility_path = _compatibility_path(output_dir) + _write_json( + compatibility_path, + _compatibility_result( + model=model, + score=score, + effective=EXPECTED_RESULT_COUNT, + ), + ) + return compatibility_path + + +def publish_failure(*, output_dir: Path, model: str, error: BaseException) -> Path: + """Publish compatibility and native diagnostics without hiding partial output.""" + output_dir.mkdir(parents=True, exist_ok=True) + native_report_path = output_dir / NATIVE_REPORT_FILENAME + native_results_path = output_dir / NATIVE_RESULTS_FILENAME + if not native_report_path.exists(): + _write_json( + native_report_path, + { + "verifier": ADAPTER_NAME, + "task": TASK_NAME, + "model": model, + "completed": False, + "threshold": 0.0, + "source": { + "ref": UPSTREAM_REF, + "sample_sha256": EXPECTED_SAMPLE_SHA256, + }, + "success_count": 0, + "failure_count": EXPECTED_RESULT_COUNT, + "tool_calls_match_rate": 0.0, + "integration_error": _error_dict(error), + }, + ) + if not native_results_path.exists(): + native_results_path.write_text( + json.dumps( + { + "status": "integration_error", + "model": model, + "error": _error_dict(error), + }, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + compatibility_path = _compatibility_path(output_dir) + _write_json( + compatibility_path, + _compatibility_result( + model=model, + score=0.0, + effective=0, + integration_error=error, + ), + ) + return compatibility_path + + +def run_full_suite( + *, + python: Path, + source_dir: Path, + dependency_dir: Path, + base_url: str, + model: str, + output_dir: Path, + runner: Runner = subprocess.run, +) -> bool: + """Run upstream once, then classify transport versus diagnostic failures.""" + output_dir.mkdir(parents=True, exist_ok=True) + for filename in (NATIVE_REPORT_FILENAME, NATIVE_RESULTS_FILENAME): + (output_dir / filename).unlink(missing_ok=True) + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + try: + verify_source_tree(source_dir) + command = build_verifier_command( + python=python, + source_dir=source_dir, + base_url=base_url, + model=model, + output_dir=output_dir, + ) + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(source_dir), str(dependency_dir)) + ) + environment["PYTHONNOUSERSITE"] = "1" + completed = runner( + command, + env=environment, + timeout=UPSTREAM_TIMEOUT_SECONDS, + check=False, + ) + if completed.returncode != 0: + raise FullSuiteError( + f"pinned upstream verifier exited with code {completed.returncode}" + ) + project_native_artifacts(output_dir=output_dir, model=model) + except (OSError, ValueError, FullSuiteError, subprocess.TimeoutExpired) as exc: + publish_failure(output_dir=output_dir, model=model, error=exc) + return False + return True + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run or project the pinned full MiniMax M3 verifier." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + prepare = subparsers.add_parser("prepare-source") + prepare.add_argument("--source-dir", required=True, type=Path) + + run = subparsers.add_parser("run") + run.add_argument("--python", required=True, type=Path) + run.add_argument("--source-dir", required=True, type=Path) + run.add_argument("--dependency-dir", required=True, type=Path) + run.add_argument("--base-url", required=True) + run.add_argument("--model", required=True) + run.add_argument("--output-dir", required=True, type=Path) + + project = subparsers.add_parser("project") + project.add_argument("--model", required=True) + project.add_argument("--output-dir", required=True, type=Path) + + failure = subparsers.add_parser("failure") + failure.add_argument("--model", required=True) + failure.add_argument("--output-dir", required=True, type=Path) + failure.add_argument("--message", required=True) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + if args.command == "prepare-source": + try: + prepare_source_tree(args.source_dir) + except (OSError, ValueError, FullSuiteError) as exc: + print(f"ERROR: {exc}", file=os.sys.stderr) + return 1 + return 0 + if args.command == "failure": + publish_failure( + output_dir=args.output_dir, + model=args.model, + error=FullSuiteError(args.message), + ) + return 0 + if args.command == "project": + try: + project_native_artifacts(output_dir=args.output_dir, model=args.model) + except (OSError, ValueError, FullSuiteError) as exc: + publish_failure(output_dir=args.output_dir, model=args.model, error=exc) + print(f"ERROR: {exc}", file=os.sys.stderr) + return 1 + return 0 + completed = run_full_suite( + python=args.python, + source_dir=args.source_dir, + dependency_dir=args.dependency_dir, + base_url=args.base_url, + model=args.model, + output_dir=args.output_dir, + ) + return 0 if completed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index fd2fdcdaee..43b3c82393 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -24,6 +24,41 @@ def _report(stream_status: str = "passed") -> dict[str, Any]: ], } +def _full_report(*, failed_records: int = 0) -> dict[str, Any]: + selected_cases: list[dict[str, Any]] = [] + results: list[dict[str, Any]] = [] + for line in range(1, kve.FULL_SELECTED_CASES + 1): + selected_cases.append( + { + "suite": "TestSchema", + "line": line, + "selection_reason": "all", + "schema": {}, + } + ) + for mode in ("non-stream", "stream"): + results.append( + { + "suite": "TestSchema", + "line": line, + "mode": mode, + "status": ( + "failed" if len(results) < failed_records else "passed" + ), + } + ) + return { + "summary": { + "total": len(results), + "by_status": { + "passed": len(results) - failed_records, + "failed": failed_records, + }, + }, + "selected_cases": selected_cases, + "results": results, + } + def _report_with_inconsistent_counts() -> dict[str, Any]: report = _report("failed") @@ -41,12 +76,12 @@ def _result(output_dir: Path) -> dict[str, Any]: return json.loads(paths[0].read_text()) -def _score(output_dir: Path) -> float: - return _result(output_dir)["results"][kve.TASK_NAME]["exact_match,strict-match"] +def _score(output_dir: Path, task_name: str = kve.TASK_NAME) -> float: + return _result(output_dir)["results"][task_name]["exact_match,strict-match"] -def _n_eff(output_dir: Path) -> int: - return _result(output_dir)["n-samples"][kve.TASK_NAME]["effective"] +def _n_eff(output_dir: Path, task_name: str = kve.TASK_NAME) -> int: + return _result(output_dir)["n-samples"][task_name]["effective"] def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: @@ -91,6 +126,49 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: str(report), ] +def test_builds_full_upstream_pytest_command(tmp_path: Path) -> None: + report = tmp_path / kve.NATIVE_REPORT_FILENAME + + assert kve.build_pytest_command( + base_url="http://127.0.0.1:8000/v1", + api_key="EMPTY", + model="test-model", + report_path=report, + task_name=kve.FULL_TASK_NAME, + ) == [ + sys.executable, + "-m", + "pytest", + "tests/tool_call_json_schema/test_tool_call_json_schema.py", + "-n", + "8", + "--reruns", + "6", + "--reruns-delay", + "3", + "--only-rerun", + ( + r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" + r"APITimeoutError|Connection error|timed out)" + ), + "--base-url", + "http://127.0.0.1:8000/v1", + "--api-key", + "EMPTY", + "--smoke-model", + "test-model", + "--think-mode", + "none", + "--selection", + "all", + "--case-dir", + "testdata/walle_validator_cases/validator_cases", + "--max-tokens", + "2048", + "--tool-json-report", + str(report), + ] + def test_builds_dsv4_thinking_command(tmp_path: Path) -> None: command = kve.build_pytest_command( @@ -160,6 +238,76 @@ def fake_run( assert "lm_eval_version" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes +def test_full_report_projects_all_mode_records_and_defers_quality_gating( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + output_dir = tmp_path / "output" + native_bytes = json.dumps(_full_report(failed_records=1)).encode() + invocation: dict[str, Any] = {} + + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + invocation.update(command=command, cwd=cwd, check=check, timeout=timeout) + Path(command[command.index("--tool-json-report") + 1]).write_bytes(native_bytes) + return SimpleNamespace(returncode=1) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + + assert kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + task_name=kve.FULL_TASK_NAME, + timeout_seconds=kve.FULL_TIMEOUT_SECONDS, + ) + projected = _result(output_dir) + assert invocation["timeout"] == 7200 + assert set(projected["results"]) == {kve.FULL_TASK_NAME} + assert _score(output_dir, kve.FULL_TASK_NAME) == 407 / 408 + assert projected["n-samples"][kve.FULL_TASK_NAME] == { + "original": 408, + "effective": 408, + } + assert "integration_error" not in projected + assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes + + +def test_full_report_rejects_incomplete_modes( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + report = _full_report() + report["results"][-1]["mode"] = "non-stream" + + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + Path(command[command.index("--tool-json-report") + 1]).write_text( + json.dumps(report) + ) + return SimpleNamespace(returncode=1) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + output_dir = tmp_path / "output" + + assert not kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + task_name=kve.FULL_TASK_NAME, + timeout_seconds=kve.FULL_TIMEOUT_SECONDS, + ) + projected = _result(output_dir) + assert _score(output_dir, kve.FULL_TASK_NAME) == 0.0 + assert _n_eff(output_dir, kve.FULL_TASK_NAME) == 0 + assert projected["integration_error"]["type"] == "ValueError" + @pytest.mark.parametrize( ("failure", "error_type"), diff --git a/utils/evals/test_minimax_m3_full_eval.py b/utils/evals/test_minimax_m3_full_eval.py new file mode 100644 index 0000000000..93d8b7ba10 --- /dev/null +++ b/utils/evals/test_minimax_m3_full_eval.py @@ -0,0 +1,200 @@ +import hashlib +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import minimax_m3_full_eval as full + + +def _write_native_run( + output_dir: Path, + *, + model: str = "MiniMax-M3", + score: float = 0.625, + result_count: int = full.EXPECTED_RESULT_COUNT, + failed_index: int | None = None, +) -> bytes: + output_dir.mkdir(exist_ok=True) + report = { + "model": model, + "success_count": result_count - (failed_index is not None), + "failure_count": int(failed_index is not None), + "success_rate": 1.0 if failed_index is None else 0.99, + "tool_calls_match_rate": score, + "tool_calls_successful_count": 60, + "error_only_reasoning_rate": 0.0, + "language_following_valid_count": 2, + "scenario_check_pass_rate": 1.0, + } + report_bytes = (json.dumps(report, indent=4) + "\n").encode() + (output_dir / full.NATIVE_REPORT_FILENAME).write_bytes(report_bytes) + rows = [ + { + "data_index": index, + "status": "failed" if index == failed_index else "success", + } + for index in range(1, result_count + 1) + ] + (output_dir / full.NATIVE_RESULTS_FILENAME).write_text( + "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8" + ) + return report_bytes + + +def test_prepared_source_tree_requires_every_pinned_byte( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + sample = b"".join(b"{}\n" for _ in range(full.EXPECTED_RESULT_COUNT)) + contents = { + "verify.py": b"print('pinned verifier')\n", + "sample.jsonl": sample, + "validator/__init__.py": b"", + } + monkeypatch.setattr( + full, + "REQUIRED_SOURCE_SHA256", + {name: hashlib.sha256(content).hexdigest() for name, content in contents.items()}, + ) + + source_dir = tmp_path / "source" + full.prepare_source_tree(source_dir, fetcher=contents.__getitem__) + full.verify_source_tree(source_dir) + + (source_dir / "verify.py").write_text("mutated\n", encoding="utf-8") + with pytest.raises(ValueError, match="SHA256 mismatch"): + full.verify_source_tree(source_dir) + assert full.EXPECTED_SAMPLE_SHA256 == ( + "3ead102af0f888acc95867b3a9916942524b02f4f64931f020a1bfb4fee9aae2" + ) + + +def test_verifier_command_is_one_102_row_run_with_fixed_m3_settings( + tmp_path: Path, +) -> None: + command = full.build_verifier_command( + python=Path("/runtime/python"), + source_dir=Path("/runtime/source"), + base_url="http://127.0.0.1:8000/v1/", + model="MiniMax-M3", + output_dir=tmp_path, + ) + + assert command == [ + "/runtime/python", + "/runtime/source/verify.py", + "/runtime/source/sample.jsonl", + "--model", + "MiniMax-M3", + "--base-url", + "http://127.0.0.1:8000/v1", + "--api-key", + "EMPTY", + "--concurrency", + "5", + "--output", + str(tmp_path / full.NATIVE_RESULTS_FILENAME), + "--summary", + str(tmp_path / full.NATIVE_REPORT_FILENAME), + "--timeout", + "600", + "--retries", + "3", + "--extra-body", + '{"temperature":0,"top_p":1,"max_tokens":40960}', + ] + assert "pass" not in " ".join(command).lower() + assert command.count("/runtime/source/sample.jsonl") == 1 + + +def test_projects_exactly_102_results_from_native_match_rate_without_rewriting_report( + tmp_path: Path, +) -> None: + report_bytes = _write_native_run(tmp_path) + + compatibility_path = full.project_native_artifacts( + output_dir=tmp_path, model="MiniMax-M3" + ) + + compatibility = json.loads(compatibility_path.read_text(encoding="utf-8")) + assert compatibility["result_format"] == "inferencex-eval-v1" + assert compatibility["eval_adapter"] == "minimax-provider-verifier" + assert compatibility["results"] == { + "minimax_m3_full": { + "exact_match,strict-match": 0.625, + "exact_match_stderr,strict-match": 0.0, + } + } + assert compatibility["configs"]["minimax_m3_full"]["native_metric"] == ( + "tool_calls_match_rate" + ) + assert compatibility["configs"]["minimax_m3_full"][ + "diagnostic_threshold" + ] == 0.0 + assert compatibility["n-samples"]["minimax_m3_full"] == { + "original": 102, + "effective": 102, + } + assert (tmp_path / full.NATIVE_REPORT_FILENAME).read_bytes() == report_bytes + assert len( + (tmp_path / full.NATIVE_RESULTS_FILENAME) + .read_text(encoding="utf-8") + .splitlines() + ) == 102 + + +@pytest.mark.parametrize( + ("result_count", "failed_index", "message"), + [ + (101, None, "exactly 102 rows"), + (102, 7, "transport failures"), + ], +) +def test_projection_rejects_incomplete_or_transport_failed_native_results( + tmp_path: Path, + result_count: int, + failed_index: int | None, + message: str, +) -> None: + _write_native_run( + tmp_path, + result_count=result_count, + failed_index=failed_index, + ) + + with pytest.raises(full.FullSuiteError, match=message): + full.project_native_artifacts(output_dir=tmp_path, model="MiniMax-M3") + + +def test_upstream_process_failure_is_nonzero_and_publishes_all_failure_artifacts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + source_dir = tmp_path / "source" + dependency_dir = tmp_path / "deps" + output_dir = tmp_path / "output" + source_dir.mkdir() + dependency_dir.mkdir() + monkeypatch.setattr(full, "verify_source_tree", lambda _path: None) + + def failed_runner(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess: + return subprocess.CompletedProcess([], 9) + + assert not full.run_full_suite( + python=Path("/runtime/python"), + source_dir=source_dir, + dependency_dir=dependency_dir, + base_url="http://127.0.0.1:8000/v1", + model="MiniMax-M3", + output_dir=output_dir, + runner=failed_runner, + ) + compatibility_path = next(output_dir.glob(full.COMPATIBILITY_GLOB)) + compatibility = json.loads(compatibility_path.read_text(encoding="utf-8")) + assert compatibility["integration_error"]["type"] == "FullSuiteError" + assert compatibility["n-samples"]["minimax_m3_full"]["effective"] == 0 + assert (output_dir / full.NATIVE_REPORT_FILENAME).is_file() + assert (output_dir / full.NATIVE_RESULTS_FILENAME).is_file() diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index aad5916f47..bc92c96b6c 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -273,6 +273,45 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: assert "DISPATCH=kimi_tool_call_schema" in result.stdout assert "METADATA=kimi_tool_call_schema" in result.stdout +def test_kimi_full_suite_dispatches_to_schema_runner() -> None: + script = r''' +source "$BENCHMARK_LIB" +_run_kimi_tool_call_schema_eval() { + printf 'DISPATCH=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" +} +EVAL_SUITE=kimi_tool_call_schema_full run_kimi_vendor_eval --port 9999 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "DISPATCH=kimi_tool_call_schema_full ARGS=<--port 9999>" in result.stdout + + +def test_minimax_full_suite_dispatches_to_full_runner() -> None: + script = r''' +source "$BENCHMARK_LIB" +_run_minimax_m3_full_eval() { + printf 'DISPATCH=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" +} +EVAL_SUITE=minimax_m3_full run_minimax_vendor_eval --port 9999 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "DISPATCH=minimax_m3_full ARGS=<--port 9999>" in result.stdout + def test_kimi_vendor_rejects_batched_concurrency() -> None: result = _run_invalid_call( @@ -502,6 +541,39 @@ def test_minimax_vendor_dependency_install_is_pinned_and_minimal( assert "--break-system-packages" not in result.stdout +def test_minimax_full_dependency_install_matches_pinned_upstream_requirements( + tmp_path: Path, +) -> None: + script = r''' +source "$BENCHMARK_LIB" +selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } +VENDOR_VERIFIER_PYTHON=selected_python +_install_minimax_m3_full_deps "$RUNTIME_DIR" +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RUNTIME_DIR": str(tmp_path / "runtime"), + }, + text=True, + capture_output=True, + check=True, + ) + + for requirement in ( + "jsonschema==4.25.1", + "loguru==0.7.3", + "megfile==4.2.5", + "numpy==2.3.4", + "openai==2.7.1", + "tqdm==4.67.1", + ): + assert f"PYTHON_ARG=<{requirement}>" in result.stdout + assert "--break-system-packages" not in result.stdout + + def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> None: results_dir = tmp_path / "results" runtime_dir = tmp_path / "runtime" @@ -964,6 +1036,7 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: assert "PYTHON_ARG=<--target>" in result.stdout assert f"PYTHON_ARG=<{runtime_dir}>" in result.stdout assert "PYTHON_ARG=" in result.stdout + assert "pytest-xdist" not in result.stdout assert "--break-system-packages" not in result.stdout @@ -1082,6 +1155,10 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( assert f"PYTHON_ARG=<{value}>" in output assert "PYTHON_ARG=<--model-prefix>" in output assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=<--task-name>" in output + assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=<--timeout-seconds>" in output + assert "PYTHON_ARG=<900>" in output assert "must-not-be-forwarded" not in output assert "SYSTEM_PYTHON_UNEXPECTED" not in output assert "EVAL_SUITE=kimi_tool_call_schema" in output @@ -1090,6 +1167,73 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( assert not verifier_dir.exists() assert not python_dir.exists() +def test_kimi_full_runner_installs_xdist_sets_timeout_and_cleans_runtimes( + tmp_path: Path, +) -> None: + results_dir = tmp_path / "results" + verifier_dir = tmp_path / "verifier" + runtime_dir = tmp_path / "runtime" + python_dir = tmp_path / "python" + script = r''' +source "$BENCHMARK_LIB" +_prepare_vendor_verifier_python() { + mkdir "$PYTHON_DIR" + VENDOR_VERIFIER_PYTHON=selected_python + VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" + export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR +} +_prepare_kimi_vendor_runtime() { + printf 'RUNTIME_SUITE=<%s>\n' "$1" >&2 + mkdir "$RUNTIME_DIR" + _install_kimi_vendor_eval_deps "$RUNTIME_DIR" "$1" >&2 + printf '%s\n' "$RUNTIME_DIR" +} +_prepare_kimi_vendor_verifier() { + mkdir "$VERIFIER_DIR" + printf '%s\n' "$VERIFIER_DIR" +} +selected_python() { + printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" >&2 + printf 'PYTHON_ARG=<%s>\n' "$@" >&2 +} +run_kimi_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" +printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" +printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "EVAL_SUITE": "kimi_tool_call_schema_full", + "RESULTS_DIR": str(results_dir), + "VERIFIER_DIR": str(verifier_dir), + "MODEL": "test-model", + "RUNTIME_DIR": str(runtime_dir), + "PYTHON_DIR": str(python_dir), + } + for key in ("EVAL_RESULT_DIR", "MODEL_NAME"): + env.pop(key, None) + + result = subprocess.run( + ["bash", "-c", script], + env=env, + text=True, + capture_output=True, + check=True, + ) + output = result.stdout + result.stderr + + assert "RUNTIME_SUITE=" in output + assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=<--task-name>" in output + assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=<--timeout-seconds>" in output + assert "PYTHON_ARG=<7200>" in output + assert "EVAL_SUITE=kimi_tool_call_schema_full" in output + assert f"EVAL_RESULT_DIR={results_dir}" in output + assert not runtime_dir.exists() + assert not verifier_dir.exists() + assert not python_dir.exists() + def test_run_lm_eval_rejects_missing_option_value(): result = _run_invalid_call("run_lm_eval --port") diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index 08899f9d43..d45c46e1db 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -2,7 +2,9 @@ "default": { "gsm8k": 0.90, "kimi_tool_call_schema": 1.0, + "kimi_tool_call_schema_full": 0.0, "minimax_m3_smoke": 1.0, + "minimax_m3_full": 0.0, "bfcl_smoke": 0.75, "bfcl_simple_python": 0.0, "bfcl_multiple": 0.0, From 830c05a0b023e04f979a604c44f719f39b7b5336 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:44:11 -0500 Subject: [PATCH 52/99] feat: add recommended BFCL eval suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增推荐的 BFCL 评估套件,并保留完整的分类诊断和上游原始产物。 --- .../workflows/benchmark-multinode-tmpl.yml | 4 +- .github/workflows/benchmark-tmpl.yml | 4 +- .github/workflows/e2e-tests.yml | 4 +- benchmarks/benchmark_lib.sh | 142 +++++- .../multi_node/amd_utils/server_atom.sh | 22 +- .../multi_node/amd_utils/server_sglang.sh | 22 +- .../multi_node/amd_utils/server_vllm.sh | 22 +- runners/launch_h200-dgxc-slurm.sh | 3 +- runners/patch_srt_eval_dispatch.py | 2 +- runners/test_slurm_utils.py | 4 + utils/evals/bfcl_eval.py | 424 +++++++++++++++--- utils/evals/test_bfcl_eval.py | 320 ++++++++++++- utils/evals/test_run_eval_dispatch.py | 327 +++++++++++++- utils/evals/thresholds.yaml | 15 + 14 files changed, 1206 insertions(+), 109 deletions(-) diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 48fd1951fc..970417a9da 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -144,7 +144,7 @@ on: required: false default: "lm-eval" eval-suite: - description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" type: string required: false default: "" @@ -499,6 +499,7 @@ jobs: predictions.jsonl swebench_report_*.json bfcl_report.json + bfcl_upstream_artifacts.tar.gz *.traj* if-no-files-found: ${{ inputs.eval-only && 'error' || 'ignore' }} @@ -523,6 +524,7 @@ jobs: rm -f -- ./*_vendor_report.json || true rm -f -- ./*_vendor_results.jsonl || true rm -f bfcl_report.json || true + rm -f bfcl_upstream_artifacts.tar.gz || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 6d41a2002d..4853a9b96a 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -96,7 +96,7 @@ on: required: false default: "lm-eval" eval-suite: - description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" type: string required: false default: "" @@ -416,6 +416,7 @@ jobs: *_vendor_report.json *_vendor_results.jsonl bfcl_report.json + bfcl_upstream_artifacts.tar.gz sample*.jsonl agent_preds.json predictions.jsonl @@ -442,6 +443,7 @@ jobs: rm -f -- ./*_vendor_results.jsonl || true rm -f sample*.jsonl || true rm -f bfcl_report.json || true + rm -f bfcl_upstream_artifacts.tar.gz || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true - name: Resource cleanup (post-run) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 052fc69c0b..38b3a2fe16 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -51,7 +51,7 @@ on: type: string default: "lm-eval" eval-suite: - description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" required: false type: string default: "" @@ -141,7 +141,7 @@ on: type: string default: "lm-eval" eval-suite: - description: "Eval suite (kimi_tool_call_schema, minimax_m3_smoke, or bfcl_smoke); empty for lm-eval and swebench" + description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" required: false type: string default: "" diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index ff61d10579..4613655208 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1417,11 +1417,61 @@ _prepare_bfcl_runtime() { printf '%s\n' "$runtime_dir" } +_archive_bfcl_upstream_artifacts() { + local project_root="$1" + local archive_path="$2" + + "${VENDOR_VERIFIER_PYTHON:-python3}" - "$project_root" "$archive_path" <<'PY' +import gzip +import os +from pathlib import Path +import tarfile +import sys + +project_root = Path(sys.argv[1]) +archive_path = Path(sys.argv[2]) +temporary_path = archive_path.with_name(f".{archive_path.name}.tmp") +temporary_path.unlink(missing_ok=True) + +try: + with ( + temporary_path.open("xb") as raw_archive, + gzip.GzipFile(filename="", mode="wb", fileobj=raw_archive, mtime=0) as compressed, + tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive, + ): + for path in sorted( + project_root.rglob("*"), + key=lambda candidate: candidate.relative_to(project_root).as_posix(), + ): + relative_path = path.relative_to(project_root).as_posix() + if path.is_symlink(): + raise ValueError(f"refusing to archive symbolic link: {relative_path}") + info = archive.gettarinfo(str(path), arcname=relative_path) + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + if info.isdir(): + archive.addfile(info) + elif info.isfile(): + with path.open("rb") as source: + archive.addfile(info, source) + else: + raise ValueError(f"refusing to archive special file: {relative_path}") + os.replace(temporary_path, archive_path) +except BaseException: + temporary_path.unlink(missing_ok=True) + raise +PY +} + _write_bfcl_integration_error() { local adapter_path="$1" local model_name="$2" local results_dir="$3" local message="$4" + local suite="$5" local adapter_rc=0 # Integration errors deliberately make the adapter exit nonzero after @@ -1430,6 +1480,7 @@ _write_bfcl_integration_error() { "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ --model "$model_name" \ --output-dir "$results_dir" \ + --suite "$suite" \ --integration-error "$message" \ || adapter_rc=$? if [ -f "${results_dir}/bfcl_report.json" ] \ @@ -1442,7 +1493,13 @@ _write_bfcl_integration_error() { return "$adapter_rc" } -_run_bfcl_smoke_eval() { +_run_bfcl_suite_eval() { + local eval_suite="$1" + local num_threads="$2" + local process_timeout_seconds="$3" + local archive_upstream="$4" + shift 4 + local port="${PORT:-8888}" local results_dir="${EVAL_RESULT_DIR:-}" @@ -1478,6 +1535,9 @@ _run_bfcl_smoke_eval() { mkdir -p "$results_dir" || return $? results_dir="$(cd "$results_dir" && pwd)" || return $? export EVAL_RESULT_DIR="$results_dir" + if [ "$archive_upstream" = true ]; then + rm -f "${results_dir}/bfcl_upstream_artifacts.tar.gz" + fi local setup_rc=0 integration_error="" _prepare_vendor_verifier_python "BFCL" "bfcl-python" true 10 || { @@ -1501,7 +1561,7 @@ _run_bfcl_smoke_eval() { local artifact_rc=0 _write_bfcl_integration_error \ "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ - || artifact_rc=$? + "$eval_suite" || artifact_rc=$? if [ "$artifact_rc" -ne 0 ]; then echo "ERROR: failed to write BFCL failure artifact (exit code ${artifact_rc})" >&2 fi @@ -1511,15 +1571,30 @@ _run_bfcl_smoke_eval() { fi local eval_rc=0 - timeout 900 "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ + local -a suite_args=() + if [ "$eval_suite" != "bfcl_smoke" ]; then + suite_args=(--suite "$eval_suite") + fi + timeout "$process_timeout_seconds" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ --base-url "http://127.0.0.1:${port}/v1" \ --api-key EMPTY \ --model "$model_name" \ --output-dir "$results_dir" \ --bfcl-project-root "$project_root" \ - --num-threads 4 \ + "${suite_args[@]}" \ + --num-threads "$num_threads" \ --request-timeout-seconds 180 \ || eval_rc=$? + local archive_rc=0 + if [ "$archive_upstream" = true ]; then + _archive_bfcl_upstream_artifacts \ + "$project_root" "${results_dir}/bfcl_upstream_artifacts.tar.gz" \ + || archive_rc=$? + if [ "$archive_rc" -ne 0 ]; then + echo "ERROR: failed to archive BFCL upstream artifacts (exit code ${archive_rc})" >&2 + fi + fi if [ "$eval_rc" -ne 0 ] \ && { [ ! -f "${results_dir}/bfcl_report.json" ] \ || [ ! -f "${results_dir}/results_bfcl.json" ]; }; then @@ -1527,14 +1602,22 @@ _run_bfcl_smoke_eval() { local artifact_rc=0 _write_bfcl_integration_error \ "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ - || artifact_rc=$? + "$eval_suite" || artifact_rc=$? if [ "$artifact_rc" -ne 0 ]; then echo "ERROR: failed to write BFCL failure artifact (exit code ${artifact_rc})" >&2 fi fi _cleanup_vendor_eval \ "$runtime_dir" "$project_root" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" - return "$eval_rc" + if [ "$eval_rc" -ne 0 ]; then + return "$eval_rc" + fi + return "$archive_rc" +} + + +_run_bfcl_smoke_eval() { + _run_bfcl_suite_eval bfcl_smoke 4 900 false "$@" } run_bfcl_eval() { @@ -1545,6 +1628,12 @@ run_bfcl_eval() { bfcl_smoke) _run_bfcl_smoke_eval "$@" ;; + bfcl_vllm_minimax_m3) + _run_bfcl_suite_eval "$eval_suite" 8 7200 true "$@" + ;; + bfcl_vllm_kimi) + _run_bfcl_suite_eval "$eval_suite" 16 7200 true "$@" + ;; *) echo "ERROR: unsupported BFCL suite '${eval_suite}'" >&2 export EVAL_RESULT_DIR="" @@ -2219,6 +2308,11 @@ append_lm_eval_summary() { fi done < <(find "${out_dir}" -type f -name "*.json*" -print0 2>/dev/null) fi + if [ -f "${out_dir}/bfcl_upstream_artifacts.tar.gz" ] \ + && ! mv -f "${out_dir}/bfcl_upstream_artifacts.tar.gz" ./; then + echo "ERROR: failed to move ${out_dir}/bfcl_upstream_artifacts.tar.gz" >&2 + return 1 + fi # Best-effort cleanup of the temp directory if [ -n "${out_dir}" ] && [ -d "${out_dir}" ]; then @@ -2228,6 +2322,31 @@ append_lm_eval_summary() { echo "Moved eval artifacts to: $(pwd)" } +stage_eval_artifacts() { + local destination="$1" + shift + + mkdir -p "$destination" || return $? + local source_dir artifact + local artifacts=() + for source_dir in "$@"; do + [ -d "$source_dir" ] || continue + artifacts=( + "$source_dir"/meta_env.json + "$source_dir"/results*.json + "$source_dir"/*_vendor_report.json + "$source_dir"/*_vendor_results.jsonl + "$source_dir"/bfcl_report.json + "$source_dir"/bfcl_upstream_artifacts.tar.gz + "$source_dir"/sample*.jsonl + ) + for artifact in "${artifacts[@]}"; do + [ -f "$artifact" ] || continue + cp -f "$artifact" "$destination/" || return $? + done + done +} + _install_swebench_agent_deps() { python3 -m pip install -q --no-cache-dir --break-system-packages \ @@ -2715,6 +2834,7 @@ run_eval() { export EVAL_COMPLETED_SUITE="$EVAL_SUITE" fi + local stage_rc=0 # Agentic eval-only recipes have no separate staging step. Verifier failures # also carry diagnostic score artifacts to preserve. if { [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; } \ @@ -2722,17 +2842,21 @@ run_eval() { || [ "$framework" = "minimax-vendor" ] \ || [ "$framework" = "bfcl" ]; } \ && [ "$eval_rc" -ne 0 ]; }; then - append_lm_eval_summary || true + append_lm_eval_summary || stage_rc=$? fi if [ "$eval_rc" -ne 0 ]; then echo "ERROR: run_eval failed with exit code $eval_rc" >&2 if [ "${EVAL_ONLY:-false}" = "true" ]; then echo "Eval-only mode: failing after artifact collection" >&2 - return "$eval_rc" fi + return "$eval_rc" fi - return $eval_rc + if [ "$stage_rc" -ne 0 ]; then + echo "ERROR: eval artifact staging failed with exit code $stage_rc" >&2 + return "$stage_rc" + fi + return 0 } diff --git a/benchmarks/multi_node/amd_utils/server_atom.sh b/benchmarks/multi_node/amd_utils/server_atom.sh index 0ddb04ce29..5bad8fd39b 100755 --- a/benchmarks/multi_node/amd_utils/server_atom.sh +++ b/benchmarks/multi_node/amd_utils/server_atom.sh @@ -417,7 +417,7 @@ if [ "$NODE_RANK" -eq 0 ]; then eval_rc=$? if [[ $eval_rc -ne 0 ]]; then - echo "ERROR: run_eval exited rc=$eval_rc; skipping metadata write and eval artifact staging" >&2 + echo "ERROR: run_eval exited rc=$eval_rc; preserving failure artifacts" >&2 EVAL_FAILED=1 else export TP="${PREFILL_TP_SIZE}" @@ -433,17 +433,15 @@ if [ "$NODE_RANK" -eq 0 ]; then MODEL_NAME="${MODEL_DIR}/${MODEL_NAME}" append_lm_eval_summary - EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" - mkdir -p "$EVAL_COPY_DIR" - for f in meta_env.json; do - [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" - done - find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; - - echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" + fi + + EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" + if stage_eval_artifacts \ + "$EVAL_COPY_DIR" /workspace "${EVAL_RESULT_DIR:-}"; then + echo "Eval artifacts staged in $EVAL_COPY_DIR" + else + echo "ERROR: failed to stage eval artifacts in $EVAL_COPY_DIR" >&2 + EVAL_FAILED=1 fi fi diff --git a/benchmarks/multi_node/amd_utils/server_sglang.sh b/benchmarks/multi_node/amd_utils/server_sglang.sh index 762a999eb4..7815e5a911 100755 --- a/benchmarks/multi_node/amd_utils/server_sglang.sh +++ b/benchmarks/multi_node/amd_utils/server_sglang.sh @@ -1119,7 +1119,7 @@ print(json.dumps(json.loads(sys.stdin.read())))' <<<"$_val")" || { eval_rc=$? if [[ $eval_rc -ne 0 ]]; then - echo "ERROR: run_eval exited rc=$eval_rc; skipping metadata write and eval artifact staging" >&2 + echo "ERROR: run_eval exited rc=$eval_rc; preserving failure artifacts" >&2 EVAL_FAILED=1 else # Always rewrite meta_env.json so EP/DPA match the workflow @@ -1132,17 +1132,15 @@ print(json.dumps(json.loads(sys.stdin.read())))' <<<"$_val")" || { append_lm_eval_summary fi - EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" - mkdir -p "$EVAL_COPY_DIR" - for f in meta_env.json; do - [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" - done - find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; - - echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" + fi + + EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" + if stage_eval_artifacts \ + "$EVAL_COPY_DIR" /workspace "${EVAL_RESULT_DIR:-}"; then + echo "Eval artifacts staged in $EVAL_COPY_DIR" + else + echo "ERROR: failed to stage eval artifacts in $EVAL_COPY_DIR" >&2 + EVAL_FAILED=1 fi fi diff --git a/benchmarks/multi_node/amd_utils/server_vllm.sh b/benchmarks/multi_node/amd_utils/server_vllm.sh index f2469bdf81..5a6ce23577 100755 --- a/benchmarks/multi_node/amd_utils/server_vllm.sh +++ b/benchmarks/multi_node/amd_utils/server_vllm.sh @@ -361,7 +361,7 @@ if [ "$NODE_RANK" -eq 0 ]; then eval_rc=$? if [[ $eval_rc -ne 0 ]]; then - echo "ERROR: run_eval exited rc=$eval_rc; skipping metadata write and eval artifact staging" >&2 + echo "ERROR: run_eval exited rc=$eval_rc; preserving failure artifacts" >&2 EVAL_FAILED=1 else export TP="${PREFILL_TP_SIZE}" @@ -384,17 +384,15 @@ if [ "$NODE_RANK" -eq 0 ]; then append_lm_eval_summary - EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" - mkdir -p "$EVAL_COPY_DIR" - for f in meta_env.json; do - [ -e "/workspace/$f" ] && cp -f "/workspace/$f" "$EVAL_COPY_DIR/" - done - find /workspace -maxdepth 1 -name 'results*.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name '*_vendor_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'bfcl_report.json' -exec cp -f {} "$EVAL_COPY_DIR/" \; - find /workspace -maxdepth 1 -name 'sample*.jsonl' -exec cp -f {} "$EVAL_COPY_DIR/" \; - - echo "Eval completed. Artifacts staged in $EVAL_COPY_DIR" + fi + + EVAL_COPY_DIR="/run_logs/slurm_job-${SLURM_JOB_ID}/eval_results" + if stage_eval_artifacts \ + "$EVAL_COPY_DIR" /workspace "${EVAL_RESULT_DIR:-}"; then + echo "Eval artifacts staged in $EVAL_COPY_DIR" + else + echo "ERROR: failed to stage eval artifacts in $EVAL_COPY_DIR" >&2 + EVAL_FAILED=1 fi fi diff --git a/runners/launch_h200-dgxc-slurm.sh b/runners/launch_h200-dgxc-slurm.sh index 130d4a98ae..8e9c13e454 100755 --- a/runners/launch_h200-dgxc-slurm.sh +++ b/runners/launch_h200-dgxc-slurm.sh @@ -107,7 +107,8 @@ if [[ "$IS_MULTINODE" == "true" ]]; then git checkout sa-submission-q2-2026 fi if [[ "${EVAL_FRAMEWORK:-lm-eval}" != "lm-eval" ]]; then - python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" + python3 "$GITHUB_WORKSPACE/runners/patch_srt_eval_dispatch.py" "$(pwd)" \ + || exit 1 fi echo "Installing srtctl..." diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index a66b0a5a2d..3d9a7c76e8 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -21,7 +21,7 @@ EVAL_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" VERIFIER_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true -cp -v *_vendor_report.json bfcl_report.json /logs/eval_results/ 2>/dev/null || true +cp -v *_vendor_report.json *_vendor_results.jsonl bfcl_report.json bfcl_upstream_artifacts.tar.gz /logs/eval_results/ 2>/dev/null || true cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index cc8c501f15..5c49591b5b 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -106,6 +106,8 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert "--framework lm-eval" not in eval_script.read_text() assert "*_vendor_report.json" in eval_script.read_text() assert "bfcl_report.json" in eval_script.read_text() + assert "*_vendor_results.jsonl" in eval_script.read_text() + assert "bfcl_upstream_artifacts.tar.gz" in eval_script.read_text() assert "already patched" in second.stdout @@ -284,6 +286,8 @@ def test_nvidia_srt_launchers_prepare_kimi_eval_dispatch() -> None: for launcher in launchers: content = launcher.read_text() assert "patch_srt_eval_dispatch.py" in content + patch_command = content.index("patch_srt_eval_dispatch.py") + assert "|| exit 1" in content[patch_command : patch_command + 200] assert 'EVAL_FRAMEWORK:-lm-eval}" != "lm-eval"' in content assert "inject_synthetic_acceptance" in content diff --git a/utils/evals/bfcl_eval.py b/utils/evals/bfcl_eval.py index ada0de455b..15b10cc243 100755 --- a/utils/evals/bfcl_eval.py +++ b/utils/evals/bfcl_eval.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Run the pinned four-case BFCL V4 OpenAI chat-completions smoke.""" +"""Run pinned BFCL V4 OpenAI chat-completions suites.""" from __future__ import annotations @@ -15,6 +15,7 @@ from dataclasses import dataclass from pathlib import Path from queue import SimpleQueue +from types import MappingProxyType from typing import Any, Protocol TASK_NAME = "bfcl_smoke" @@ -36,16 +37,109 @@ SOURCE_REVISION = "6ea57973c7a6097fd7c5915698c54c17c5b1b6c8" VLLM_INTEGRATION_REF = "7ecb11405df86b202f4c5cca322bd133052fee82" -# Dict insertion order is intentional: reports and the upstream run-ID file are stable. -SMOKE_CASE_IDS: dict[str, tuple[str, ...]] = { - "simple_python": ("simple_python_141",), - "multiple": ("multiple_38",), - "parallel": ("parallel_1",), - "irrelevance": ("irrelevance_0",), -} +# Dict insertion order is intentional: smoke reports and the upstream run-ID +# file remain byte-for-byte stable. +SMOKE_CASE_IDS: Mapping[str, tuple[str, ...]] = MappingProxyType( + { + "simple_python": ("simple_python_141",), + "multiple": ("multiple_38",), + "parallel": ("parallel_1",), + "irrelevance": ("irrelevance_0",), + } +) EXPECTED_SAMPLE_COUNT = sum(len(case_ids) for case_ids in SMOKE_CASE_IDS.values()) +@dataclass(frozen=True) +class SuiteSpec: + name: str + generation_categories: tuple[str, ...] + expected_leaf_counts: tuple[tuple[str, int], ...] + temperature: float + default_num_threads: int + threshold: float + maximum_step_limit: int | None = None + category_limits: tuple[tuple[str, int], ...] = () + + @property + def leaf_categories(self) -> tuple[str, ...]: + return tuple(category for category, _ in self.expected_leaf_counts) + + @property + def expected_sample_count(self) -> int: + return sum(count for _, count in self.expected_leaf_counts) + + def projected_task(self, category: str) -> str: + if self.name == TASK_NAME: + return f"bfcl_{category}" + return f"{self.name}_{category}" + + +SMOKE_SUITE = SuiteSpec( + name=TASK_NAME, + generation_categories=tuple(SMOKE_CASE_IDS), + expected_leaf_counts=tuple( + (category, len(case_ids)) for category, case_ids in SMOKE_CASE_IDS.items() + ), + temperature=0.0, + default_num_threads=DEFAULT_NUM_THREADS, + threshold=REQUIRED_SCORE, +) +MINIMAX_SUITE = SuiteSpec( + name="bfcl_vllm_minimax_m3", + generation_categories=( + "simple_python", + "multiple", + "parallel", + "parallel_multiple", + ), + expected_leaf_counts=( + ("simple_python", 400), + ("multiple", 200), + ("parallel", 200), + ("parallel_multiple", 200), + ), + temperature=0.001, + default_num_threads=8, + threshold=0.0, +) +KIMI_SUITE = SuiteSpec( + name="bfcl_vllm_kimi", + generation_categories=( + "simple_python", + "multiple", + "parallel", + "parallel_multiple", + "multi_turn", + ), + expected_leaf_counts=( + ("simple_python", 400), + ("multiple", 200), + ("parallel", 200), + ("parallel_multiple", 200), + ("multi_turn_base", 60), + ("multi_turn_miss_func", 60), + ("multi_turn_miss_param", 60), + ("multi_turn_long_context", 60), + ), + temperature=0.001, + default_num_threads=16, + threshold=0.0, + maximum_step_limit=10, + category_limits=(("multi_turn", 240),), +) +SUITE_SPECS: Mapping[str, SuiteSpec] = MappingProxyType( + { + suite.name: suite + for suite in ( + SMOKE_SUITE, + MINIMAX_SUITE, + KIMI_SUITE, + ) + } +) + + class UpstreamRunner(Protocol): """Injectable boundary around the optional BFCL installation.""" @@ -73,6 +167,7 @@ class CategoryScore: total_count: int def as_dict(self) -> dict[str, Any]: + failure_ids = {record["id"] for record in self.records} return { "category": self.category, "case_ids": list(self.case_ids), @@ -82,8 +177,8 @@ def as_dict(self) -> dict[str, Any]: "case_scores": [ { "id": case_id, - "score": self.accuracy, - "correct": self.correct_count == self.total_count, + "score": 0.0 if case_id in failure_ids else 1.0, + "correct": case_id not in failure_ids, } for case_id in self.case_ids ], @@ -128,7 +223,9 @@ def _positive_float(value: str) -> float: return parsed -def _source_details() -> dict[str, Any]: +def _source_details( + suite: SuiteSpec, case_ids_by_category: Mapping[str, tuple[str, ...]] +) -> dict[str, Any]: return { "url": UPSTREAM_SOURCE, "repository": UPSTREAM_REPOSITORY, @@ -139,7 +236,8 @@ def _source_details() -> dict[str, Any]: "source_revision": SOURCE_REVISION, "vllm_integration_ref": VLLM_INTEGRATION_REF, "case_ids": { - category: list(case_ids) for category, case_ids in SMOKE_CASE_IDS.items() + category: list(case_ids) + for category, case_ids in case_ids_by_category.items() }, } @@ -148,7 +246,9 @@ def _error_dict(error: BaseException) -> dict[str, str]: return {"type": type(error).__name__, "message": str(error)} -def _expected_category_details() -> list[dict[str, Any]]: +def _expected_category_details( + case_ids_by_category: Mapping[str, tuple[str, ...]], +) -> list[dict[str, Any]]: return [ { "category": category, @@ -160,23 +260,29 @@ def _expected_category_details() -> list[dict[str, Any]]: {"id": case_id, "score": 0.0, "correct": False} for case_id in case_ids ], } - for category, case_ids in SMOKE_CASE_IDS.items() + for category, case_ids in case_ids_by_category.items() ] -def _diagnostics(scores: Sequence[CategoryScore] | None = None) -> dict[str, Any]: +def _diagnostics( + suite: SuiteSpec, + case_ids_by_category: Mapping[str, tuple[str, ...]], + scores: Sequence[CategoryScore] | None = None, +) -> dict[str, Any]: return { - "source": _source_details(), + "source": _source_details(suite, case_ids_by_category), "categories": ( [score.as_dict() for score in scores] if scores is not None - else _expected_category_details() + else _expected_category_details(case_ids_by_category) ), } def _native_report( *, + suite: SuiteSpec, + case_ids_by_category: Mapping[str, tuple[str, ...]], model: str, base_url: str | None, num_threads: int, @@ -188,20 +294,23 @@ def _native_report( accuracy = correct_count / total_count if total_count else 0.0 report: dict[str, Any] = { "verifier": ADAPTER_NAME, - "task": TASK_NAME, + "task": suite.name, "model": model, "endpoint": base_url, "completed": integration_error is None, - "passed": integration_error is None and accuracy >= REQUIRED_SCORE, - "threshold": REQUIRED_SCORE, - "sampling": {"temperature": 0.0, "num_threads": num_threads}, + "passed": integration_error is None and accuracy >= suite.threshold, + "threshold": suite.threshold, + "sampling": { + "temperature": suite.temperature, + "num_threads": num_threads, + }, "summary": { "accuracy": accuracy, "correct_count": correct_count, "total_count": total_count, - "expected_count": EXPECTED_SAMPLE_COUNT, + "expected_count": suite.expected_sample_count, }, - "bfcl": _diagnostics(scores), + "bfcl": _diagnostics(suite, case_ids_by_category, scores), } if integration_error is not None: report["integration_error"] = _error_dict(integration_error) @@ -210,6 +319,8 @@ def _native_report( def _compatibility_result( *, + suite: SuiteSpec, + case_ids_by_category: Mapping[str, tuple[str, ...]], model: str, scores: Sequence[CategoryScore] | None, integration_error: BaseException | None = None, @@ -218,25 +329,45 @@ def _compatibility_result( total_count = sum(score.total_count for score in scores or ()) correct_count = sum(score.correct_count for score in scores or ()) accuracy = correct_count / total_count if total_count else 0.0 - task_scores = {TASK_NAME: accuracy} + task_scores = {suite.name: accuracy} task_samples = { - TASK_NAME: { - "original": EXPECTED_SAMPLE_COUNT, + suite.name: { + "original": suite.expected_sample_count, "effective": total_count, } } - for category, case_ids in SMOKE_CASE_IDS.items(): + for category, expected_count in suite.expected_leaf_counts: category_score = score_by_category.get(category) - task_name = f"bfcl_{category}" + task_name = suite.projected_task(category) task_scores[task_name] = ( category_score.accuracy if category_score is not None else 0.0 ) task_samples[task_name] = { - "original": len(case_ids), + "original": expected_count, "effective": ( category_score.total_count if category_score is not None else 0 ), } + + if suite is KIMI_SUITE: + multi_turn_categories = suite.leaf_categories[-4:] + multi_turn_scores = [ + score_by_category[category] + for category in multi_turn_categories + if category in score_by_category + ] + multi_turn_total = sum(score.total_count for score in multi_turn_scores) + multi_turn_correct = sum(score.correct_count for score in multi_turn_scores) + multi_turn_accuracy = ( + multi_turn_correct / multi_turn_total if multi_turn_total else 0.0 + ) + aggregate_task = suite.projected_task("multi_turn") + task_scores[aggregate_task] = multi_turn_accuracy + task_samples[aggregate_task] = { + "original": 240, + "effective": multi_turn_total, + } + results = { task_name: { "acc,none": task_score, @@ -258,7 +389,7 @@ def _compatibility_result( "results": results, "configs": configs, "n-samples": task_samples, - "bfcl": _diagnostics(scores), + "bfcl": _diagnostics(suite, case_ids_by_category, scores), } if integration_error is not None: result["integration_error"] = _error_dict(integration_error) @@ -282,11 +413,16 @@ def _prepare_output_paths(output_dir: Path) -> tuple[Path, Path]: return native_path, compatibility_path -def _write_smoke_id_map(project_root: Path) -> None: +def _write_id_map( + project_root: Path, case_ids_by_category: Mapping[str, tuple[str, ...]] +) -> None: project_root.mkdir(parents=True, exist_ok=True) _write_json( project_root / "test_case_ids_to_generate.json", - {category: list(case_ids) for category, case_ids in SMOKE_CASE_IDS.items()}, + { + category: list(case_ids) + for category, case_ids in case_ids_by_category.items() + }, ) @@ -305,6 +441,121 @@ def _function_defaults(function: Callable[..., Any]) -> dict[str, Any]: return defaults +def _load_dataset_helpers( + maximum_step_limit: int | None, +) -> tuple[Callable[[str], Any], Callable[[list[str]], Any], Callable[[Any], Any]]: + """Import BFCL's pinned dataset helpers only when a full suite is selected.""" + adapter_directory = Path(__file__).resolve().parent + original_sys_path = sys.path[:] + sys.path[:] = [ + entry + for entry in sys.path + if Path(entry or os.curdir).resolve() != adapter_directory + ] + try: + if maximum_step_limit is not None: + import bfcl_eval.constants.default_prompts as bfcl_prompts + + # This must happen before importing utils/base_handler for multi-turn. + bfcl_prompts.MAXIMUM_STEP_LIMIT = maximum_step_limit + from bfcl_eval.utils import ( + load_dataset_entry, + parse_test_category_argument, + sort_key, + ) + finally: + sys.path[:] = original_sys_path + return load_dataset_entry, parse_test_category_argument, sort_key + + +def _build_suite_case_ids( + suite: SuiteSpec, +) -> dict[str, tuple[str, ...]]: + if suite is SMOKE_SUITE: + return dict(SMOKE_CASE_IDS) + + load_dataset_entry, parse_test_category_argument, sort_key = ( + _load_dataset_helpers(suite.maximum_step_limit) + ) + category_limits = dict(suite.category_limits) + selected_by_leaf: dict[str, tuple[str, ...]] = {} + seen_ids: set[str] = set() + for category in suite.generation_categories: + leaf_categories = list(parse_test_category_argument([category])) + by_leaf = { + leaf: sorted(load_dataset_entry(leaf), key=sort_key) + for leaf in leaf_categories + } + limit = category_limits.get(category) + if limit is None: + quotas = [len(by_leaf[leaf]) for leaf in leaf_categories] + else: + base, extra = divmod(limit, len(leaf_categories)) + quotas = [ + base + (index < extra) for index in range(len(leaf_categories)) + ] + for leaf, quota in zip(leaf_categories, quotas, strict=True): + entries = by_leaf[leaf][:quota] + ids: list[str] = [] + for entry in entries: + if not isinstance(entry, Mapping): + raise ValueError(f"{leaf} dataset entry must be a mapping") + case_id = entry.get("id") + if not isinstance(case_id, str) or not case_id: + raise ValueError( + f"{leaf} dataset entry must contain a non-empty string id" + ) + if case_id in seen_ids: + raise ValueError(f"BFCL dataset contains duplicate id {case_id}") + seen_ids.add(case_id) + ids.append(case_id) + selected_by_leaf[leaf] = tuple(ids) + + actual_counts = { + category: len(case_ids) + for category, case_ids in selected_by_leaf.items() + } + expected_counts = dict(suite.expected_leaf_counts) + if actual_counts != expected_counts: + raise ValueError( + f"{suite.name} selected leaf counts {actual_counts!r}; " + f"expected {expected_counts!r}" + ) + return { + category: selected_by_leaf[category] + for category, _ in suite.expected_leaf_counts + } + + +def _read_selected_suite( + project_root: Path, +) -> tuple[SuiteSpec, dict[str, tuple[str, ...]]]: + raw = json.loads( + (project_root / "test_case_ids_to_generate.json").read_text(encoding="utf-8") + ) + if not isinstance(raw, Mapping): + raise ValueError("BFCL test-case ID map must be a JSON object") + case_ids_by_category: dict[str, tuple[str, ...]] = {} + for category, case_ids in raw.items(): + if not isinstance(category, str) or not isinstance(case_ids, list): + raise ValueError("BFCL test-case ID map has an invalid category entry") + if not all(isinstance(case_id, str) and case_id for case_id in case_ids): + raise ValueError(f"{category} test-case IDs must be non-empty strings") + case_ids_by_category[category] = tuple(case_ids) + + shape = tuple( + (category, len(case_ids)) + for category, case_ids in case_ids_by_category.items() + ) + for suite in SUITE_SPECS.values(): + if shape != suite.expected_leaf_counts: + continue + if suite is SMOKE_SUITE and case_ids_by_category != dict(SMOKE_CASE_IDS): + continue + return suite, case_ids_by_category + raise ValueError(f"test-case ID map does not match a supported suite: {shape!r}") + + def _run_upstream( *, model: str, @@ -315,6 +566,7 @@ def _run_upstream( request_timeout_seconds: float, ) -> None: """Lazily load and invoke the pinned BFCL API against an existing server.""" + suite, case_ids_by_category = _read_selected_suite(project_root) os.environ["BFCL_PROJECT_ROOT"] = str(project_root) os.environ["OPENAI_BASE_URL"] = base_url os.environ["OPENAI_API_KEY"] = api_key @@ -329,6 +581,10 @@ def _run_upstream( if Path(entry or os.curdir).resolve() != adapter_directory ] try: + if suite.maximum_step_limit is not None: + import bfcl_eval.constants.default_prompts as bfcl_prompts + + bfcl_prompts.MAXIMUM_STEP_LIMIT = suite.maximum_step_limit import bfcl_eval.constants.model_config as bfcl_model_config from bfcl_eval.__main__ import evaluate, generate from bfcl_eval.constants.model_config import ModelConfig @@ -369,12 +625,12 @@ def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: underscore_to_dot=True, ) - categories = list(SMOKE_CASE_IDS) + categories = list(suite.generation_categories) generation_kwargs = _function_defaults(generate) generation_kwargs.update( model=[model], test_category=categories, - temperature=0.0, + temperature=suite.temperature, num_threads=num_threads, skip_server_setup=True, run_ids=True, @@ -383,7 +639,7 @@ def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: generate(**generation_kwargs) if not request_failures.empty(): raise request_failures.get() - _validate_generated_results(project_root) + _validate_generated_results(project_root, case_ids_by_category) evaluation_kwargs = _function_defaults(evaluate) evaluation_kwargs.update( @@ -394,8 +650,11 @@ def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: evaluate(**evaluation_kwargs) -def _validate_generated_results(project_root: Path) -> None: - for category, case_ids in SMOKE_CASE_IDS.items(): +def _validate_generated_results( + project_root: Path, + case_ids_by_category: Mapping[str, tuple[str, ...]] = SMOKE_CASE_IDS, +) -> None: + for category, case_ids in case_ids_by_category.items(): matches = sorted(project_root.glob(f"result/**/BFCL_v4_{category}_result.json")) if len(matches) != 1: raise ValueError( @@ -482,9 +741,12 @@ def _validate_header( return float(accuracy), correct_count, total_count -def _collect_scores(project_root: Path) -> list[CategoryScore]: +def _collect_scores( + project_root: Path, + case_ids_by_category: Mapping[str, tuple[str, ...]] = SMOKE_CASE_IDS, +) -> list[CategoryScore]: scores: list[CategoryScore] = [] - for category, case_ids in SMOKE_CASE_IDS.items(): + for category, case_ids in case_ids_by_category.items(): matches = sorted(project_root.glob(f"score/**/BFCL_v4_{category}_score.json")) if len(matches) != 1: raise ValueError( @@ -557,16 +819,27 @@ def _collect_scores(project_root: Path) -> list[CategoryScore]: def publish_integration_error( - *, output_dir: Path, model: str, error: BaseException + *, + output_dir: Path, + model: str, + error: BaseException, + suite: SuiteSpec = SMOKE_SUITE, ) -> None: """Publish required zero-score artifacts without importing BFCL or Typer.""" native_path, compatibility_path = _prepare_output_paths(output_dir) + case_ids_by_category = ( + dict(SMOKE_CASE_IDS) + if suite is SMOKE_SUITE + else {category: () for category in suite.leaf_categories} + ) _write_json( native_path, _native_report( + suite=suite, + case_ids_by_category=case_ids_by_category, model=model, base_url=None, - num_threads=DEFAULT_NUM_THREADS, + num_threads=suite.default_num_threads, scores=None, integration_error=error, ), @@ -574,6 +847,8 @@ def publish_integration_error( _write_json( compatibility_path, _compatibility_result( + suite=suite, + case_ids_by_category=case_ids_by_category, model=model, scores=None, integration_error=error, @@ -588,19 +863,32 @@ def run_evaluation( model: str, output_dir: Path, bfcl_project_root: Path, - num_threads: int = DEFAULT_NUM_THREADS, + suite: SuiteSpec = SMOKE_SUITE, + num_threads: int | None = None, request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, upstream_runner: UpstreamRunner = _run_upstream, ) -> bool: - """Run the exact smoke IDs and always publish native and compatibility reports.""" + """Run one immutable BFCL suite and always publish both report formats.""" native_path, compatibility_path = _prepare_output_paths(output_dir) + selected_case_ids: dict[str, tuple[str, ...]] = ( + dict(SMOKE_CASE_IDS) + if suite is SMOKE_SUITE + else {category: () for category in suite.leaf_categories} + ) + resolved_num_threads = ( + suite.default_num_threads if num_threads is None else num_threads + ) try: + if suite.name not in SUITE_SPECS or SUITE_SPECS[suite.name] is not suite: + raise ValueError(f"unsupported BFCL suite: {suite.name}") normalized_url = _absolute_http_url(base_url) normalized_model = _nonempty_string(model) normalized_key = _nonempty_string(api_key) - if isinstance(num_threads, bool) or not isinstance(num_threads, int): + if isinstance(resolved_num_threads, bool) or not isinstance( + resolved_num_threads, int + ): raise ValueError("num_threads must be a positive integer") - if num_threads <= 0: + if resolved_num_threads <= 0: raise ValueError("num_threads must be a positive integer") if ( isinstance(request_timeout_seconds, bool) @@ -612,24 +900,32 @@ def run_evaluation( if not callable(upstream_runner): raise TypeError("upstream_runner must be callable") - _write_smoke_id_map(bfcl_project_root) + os.environ["BFCL_PROJECT_ROOT"] = str(bfcl_project_root) + selected_case_ids = _build_suite_case_ids(suite) + _write_id_map(bfcl_project_root, selected_case_ids) upstream_runner( model=normalized_model, project_root=bfcl_project_root, base_url=normalized_url, api_key=normalized_key, - num_threads=num_threads, + num_threads=resolved_num_threads, request_timeout_seconds=float(request_timeout_seconds), ) - _validate_generated_results(bfcl_project_root) - scores = _collect_scores(bfcl_project_root) + _validate_generated_results(bfcl_project_root, selected_case_ids) + scores = _collect_scores(bfcl_project_root, selected_case_ids) + if sum(score.total_count for score in scores) != suite.expected_sample_count: + raise ValueError( + f"{suite.name} evaluated an unexpected total number of cases" + ) except Exception as exc: # noqa: BLE001 - artifact publication is the boundary _write_json( native_path, _native_report( + suite=suite, + case_ids_by_category=selected_case_ids, model=model, base_url=base_url, - num_threads=num_threads, + num_threads=resolved_num_threads, scores=None, integration_error=exc, ), @@ -637,6 +933,8 @@ def run_evaluation( _write_json( compatibility_path, _compatibility_result( + suite=suite, + case_ids_by_category=selected_case_ids, model=model, scores=None, integration_error=exc, @@ -645,12 +943,19 @@ def run_evaluation( return False native = _native_report( + suite=suite, + case_ids_by_category=selected_case_ids, model=normalized_model, base_url=normalized_url, - num_threads=num_threads, + num_threads=resolved_num_threads, + scores=scores, + ) + compatibility = _compatibility_result( + suite=suite, + case_ids_by_category=selected_case_ids, + model=normalized_model, scores=scores, ) - compatibility = _compatibility_result(model=normalized_model, scores=scores) _write_json(native_path, native) _write_json(compatibility_path, compatibility) return True @@ -658,7 +963,7 @@ def run_evaluation( def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Run the pinned four-case BFCL V4 OpenAI completions smoke." + description="Run a pinned BFCL V4 OpenAI completions suite." ) parser.add_argument("--base-url", type=_absolute_http_url) parser.add_argument("--api-key", type=_nonempty_string, default="EMPTY") @@ -666,8 +971,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--output-dir", type=Path, required=True) parser.add_argument("--bfcl-project-root", type=Path) parser.add_argument( - "--num-threads", type=_positive_int, default=DEFAULT_NUM_THREADS + "--suite", + choices=tuple(SUITE_SPECS), + default=TASK_NAME, ) + parser.add_argument("--num-threads", type=_positive_int) parser.add_argument( "--request-timeout-seconds", type=_positive_float, @@ -675,6 +983,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: ) parser.add_argument("--integration-error") args = parser.parse_args(argv) + args.num_threads = ( + SUITE_SPECS[args.suite].default_num_threads + if args.num_threads is None + else args.num_threads + ) if args.integration_error is None: if args.base_url is None: parser.error("--base-url required unless --integration-error is provided") @@ -687,11 +1000,13 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) + suite = SUITE_SPECS[args.suite] if args.integration_error is not None: publish_integration_error( output_dir=args.output_dir, model=args.model, error=RuntimeError(args.integration_error), + suite=suite, ) return 1 @@ -701,6 +1016,7 @@ def main(argv: Sequence[str] | None = None) -> int: model=args.model, output_dir=args.output_dir, bfcl_project_root=args.bfcl_project_root, + suite=suite, num_threads=args.num_threads, request_timeout_seconds=args.request_timeout_seconds, ) diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index e55babc7cc..24163fd087 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -1,5 +1,6 @@ import builtins import json +import os import subprocess import sys from pathlib import Path @@ -33,6 +34,28 @@ def _score(output_dir: Path) -> float: return _compatibility(output_dir)["results"][be.TASK_NAME]["acc,none"] + +def _category_score( + category: str, + total_count: int, + correct_count: int, +) -> be.CategoryScore: + case_ids = tuple(f"{category}_{index}" for index in range(total_count)) + return be.CategoryScore( + category=category, + case_ids=case_ids, + score_file=f"score/model-a/BFCL_v4_{category}_score.json", + header={ + "accuracy": correct_count / total_count, + "correct_count": correct_count, + "total_count": total_count, + }, + records=tuple({"id": case_id} for case_id in case_ids[correct_count:]), + accuracy=correct_count / total_count, + correct_count=correct_count, + total_count=total_count, + ) + def test_thresholds_are_stdlib_readable_without_pyyaml(monkeypatch) -> None: real_import = builtins.__import__ @@ -165,11 +188,79 @@ def test_command_defaults_and_required_runtime_inputs(tmp_path: Path) -> None: assert args.num_threads == 4 assert args.request_timeout_seconds == 180.0 assert args.integration_error is None - + assert args.suite == be.TASK_NAME with pytest.raises(SystemExit): be.parse_args(["--model", "model-a", "--output-dir", str(tmp_path / "missing")]) +def test_cli_selects_suite_defaults_and_rejects_unknown_suite(tmp_path: Path) -> None: + args = be.parse_args( + [ + "--base-url", + "http://localhost:8000/v1", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + "--suite", + "bfcl_vllm_kimi", + ] + ) + + assert args.suite == "bfcl_vllm_kimi" + assert args.num_threads == 16 + + with pytest.raises(SystemExit): + be.parse_args( + [ + "--base-url", + "http://localhost:8000/v1", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + "--suite", + "bfcl_unknown", + ] + ) + + +def test_cli_forwards_selected_suite_on_normal_path( + tmp_path: Path, monkeypatch +) -> None: + invocation: dict[str, Any] = {} + + def run_evaluation(**kwargs: Any) -> bool: + invocation.update(kwargs) + return True + + monkeypatch.setattr(be, "run_evaluation", run_evaluation) + + return_code = be.main( + [ + "--base-url", + "http://localhost:8000/v1", + "--model", + "model-a", + "--output-dir", + str(tmp_path / "output"), + "--bfcl-project-root", + str(tmp_path / "bfcl"), + "--suite", + "bfcl_vllm_kimi", + ] + ) + + assert return_code == 0 + assert invocation["suite"] is be.KIMI_SUITE + assert invocation["num_threads"] == 16 + + + @pytest.mark.parametrize( ("flag", "value"), (("--num-threads", "0"), ("--request-timeout-seconds", "nan")), @@ -551,3 +642,230 @@ def test_integration_error_cli_is_stdlib_only_and_returns_nonzero( native = _native(output_dir) assert native["completed"] is False assert native["integration_error"] == compatibility["integration_error"] + + +def test_full_suite_sets_project_root_before_dataset_import( + monkeypatch, tmp_path: Path +) -> None: + output_dir = tmp_path / "output" + project_root = tmp_path / "project" + observed_roots: list[str | None] = [] + monkeypatch.setenv("BFCL_PROJECT_ROOT", "stale-root") + + def stop_after_observing_root(suite: be.SuiteSpec): + observed_roots.append(os.environ.get("BFCL_PROJECT_ROOT")) + raise RuntimeError("stop after project-root check") + + monkeypatch.setattr(be, "_build_suite_case_ids", stop_after_observing_root) + + passed = be.run_evaluation( + base_url="http://localhost:8000/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + bfcl_project_root=project_root, + suite=be.MINIMAX_SUITE, + ) + + assert passed is False + assert observed_roots == [str(project_root)] + + +@pytest.mark.parametrize( + ("suite", "expected_step_limit"), + ( + (be.MINIMAX_SUITE, None), + (be.KIMI_SUITE, 10), + ), +) +def test_full_suite_ids_use_exact_sorted_leaf_allocations( + monkeypatch, + suite: be.SuiteSpec, + expected_step_limit: int | None, +) -> None: + multi_turn_leaves = ( + "multi_turn_base", + "multi_turn_miss_func", + "multi_turn_miss_param", + "multi_turn_long_context", + ) + upstream_multi_turn_leaves = ( + "multi_turn_base", + "multi_turn_long_context", + "multi_turn_miss_func", + "multi_turn_miss_param", + ) + available_counts = { + "simple_python": 400, + "multiple": 200, + "parallel": 200, + "parallel_multiple": 200, + **{leaf: 75 for leaf in multi_turn_leaves}, + } + observed_step_limits: list[int | None] = [] + + def load_dataset_entry(category: str) -> list[dict[str, str]]: + return [ + {"id": f"{category}_{index:03d}"} + for index in reversed(range(available_counts[category])) + ] + + def parse_test_category_argument(categories: list[str]) -> list[str]: + assert len(categories) == 1 + return ( + list(upstream_multi_turn_leaves) + if categories[0] == "multi_turn" + else categories + ) + + def load_helpers(maximum_step_limit): + observed_step_limits.append(maximum_step_limit) + return ( + load_dataset_entry, + parse_test_category_argument, + lambda entry: entry["id"], + ) + + monkeypatch.setattr(be, "_load_dataset_helpers", load_helpers) + + selected = be._build_suite_case_ids(suite) + + assert observed_step_limits == [expected_step_limit] + assert tuple( + (category, len(case_ids)) for category, case_ids in selected.items() + ) == suite.expected_leaf_counts + assert sum(map(len, selected.values())) == suite.expected_sample_count + assert all( + list(case_ids) == sorted(case_ids) for case_ids in selected.values() + ) + if suite is be.KIMI_SUITE: + assert { + leaf: len(selected[leaf]) for leaf in multi_turn_leaves + } == {leaf: 60 for leaf in multi_turn_leaves} + assert all(selected[leaf][-1].endswith("_059") for leaf in multi_turn_leaves) + + +def test_mixed_category_diagnostics_project_each_failure_id() -> None: + score = be.CategoryScore( + category="parallel", + case_ids=("parallel_0", "parallel_1", "parallel_2"), + score_file="score/model-a/BFCL_v4_parallel_score.json", + header={"accuracy": 2 / 3, "correct_count": 2, "total_count": 3}, + records=({"id": "parallel_1", "error": "wrong call"},), + accuracy=2 / 3, + correct_count=2, + total_count=3, + ) + + assert score.as_dict()["case_scores"] == [ + {"id": "parallel_0", "score": 1.0, "correct": True}, + {"id": "parallel_1", "score": 0.0, "correct": False}, + {"id": "parallel_2", "score": 1.0, "correct": True}, + ] + + +def test_kimi_projects_namespaced_leaf_and_weighted_aggregate_scores() -> None: + assert be.KIMI_SUITE.expected_sample_count == 1240 + selected = { + category: tuple(f"{category}_{index}" for index in range(total_count)) + for category, total_count in be.KIMI_SUITE.expected_leaf_counts + } + correct_counts = { + "simple_python": 200, + "multiple": 100, + "parallel": 50, + "parallel_multiple": 200, + "multi_turn_base": 60, + "multi_turn_miss_func": 30, + "multi_turn_miss_param": 0, + "multi_turn_long_context": 15, + } + scores = [ + _category_score(category, total_count, correct_counts[category]) + for category, total_count in be.KIMI_SUITE.expected_leaf_counts + ] + + compatibility = be._compatibility_result( + suite=be.KIMI_SUITE, + case_ids_by_category=selected, + model="model-a", + scores=scores, + ) + + assert compatibility["results"]["bfcl_vllm_kimi"]["acc,none"] == 655 / 1240 + assert ( + compatibility["results"]["bfcl_vllm_kimi_multi_turn"]["acc,none"] + == 105 / 240 + ) + assert compatibility["n-samples"]["bfcl_vllm_kimi_multi_turn"] == { + "original": 240, + "effective": 240, + } + assert all( + f"bfcl_vllm_kimi_{category}" in compatibility["results"] + for category in selected + ) + assert "bfcl_simple_python" not in compatibility["results"] + assert "bfcl_multi_turn" not in compatibility["results"] + + +def test_selected_suite_integration_error_preserves_suite_identity( + tmp_path: Path, +) -> None: + output_dir = tmp_path / "output" + + return_code = be.main( + [ + "--model", + "model-a", + "--output-dir", + str(output_dir), + "--suite", + "bfcl_vllm_minimax_m3", + "--integration-error", + "pinned wheel installation failed", + ] + ) + + assert return_code == 1 + compatibility = _compatibility(output_dir) + native = _native(output_dir) + assert native["task"] == "bfcl_vllm_minimax_m3" + assert native["summary"]["expected_count"] == 1000 + assert native["sampling"] == {"temperature": 0.001, "num_threads": 8} + assert list(compatibility["results"]) == [ + "bfcl_vllm_minimax_m3", + "bfcl_vllm_minimax_m3_simple_python", + "bfcl_vllm_minimax_m3_multiple", + "bfcl_vllm_minimax_m3_parallel", + "bfcl_vllm_minimax_m3_parallel_multiple", + ] + assert compatibility["n-samples"]["bfcl_vllm_minimax_m3"] == { + "original": 1000, + "effective": 0, + } + assert native["integration_error"] == compatibility["integration_error"] + + +def test_score_total_must_match_every_selected_id(tmp_path: Path) -> None: + project_root = tmp_path / "bfcl" + score_path = ( + project_root + / "score" + / "model-a" + / "BFCL_v4_simple_python_score.json" + ) + score_path.parent.mkdir(parents=True) + score_path.write_text( + json.dumps({"accuracy": 1.0, "correct_count": 1, "total_count": 1}) + "\n", + encoding="utf-8", + ) + + with pytest.raises( + ValueError, + match="simple_python evaluated 1 cases; expected 2", + ): + be._collect_scores( + project_root, + {"simple_python": ("simple_python_0", "simple_python_1")}, + ) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index bc92c96b6c..919f903c30 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -7,6 +7,7 @@ import re import stat import subprocess +import sys import tarfile import threading from contextlib import contextmanager @@ -273,6 +274,30 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: assert "DISPATCH=kimi_tool_call_schema" in result.stdout assert "METADATA=kimi_tool_call_schema" in result.stdout + +def test_agentic_eval_propagates_artifact_staging_failure() -> None: + script = r''' +source "$BENCHMARK_LIB" +run_kimi_vendor_eval() { :; } +append_lm_eval_summary() { return 73; } +export EVAL_FRAMEWORK=kimi-vendor +export EVAL_ONLY=true +export IS_AGENTIC=1 +export EVAL_CONCURRENT_REQUESTS="" +unset EVAL_SUITE +run_eval --port 8888 +''' + result = subprocess.run( + ["bash", "-c", script], + env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 73 + assert "eval artifact staging failed with exit code 73" in result.stderr + def test_kimi_full_suite_dispatches_to_schema_runner() -> None: script = r''' source "$BENCHMARK_LIB" @@ -1343,6 +1368,98 @@ def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: return json.loads((work_dir / "meta_env.json").read_text()) +def test_summary_stages_bfcl_upstream_archive_before_cleanup(tmp_path: Path) -> None: + work_dir = tmp_path / "work" + results_dir = tmp_path / "results" + work_dir.mkdir() + results_dir.mkdir() + archive = results_dir / "bfcl_upstream_artifacts.tar.gz" + archive.write_bytes(b"bfcl-archive") + script = r''' +source "$BENCHMARK_LIB" +cd "$WORK_DIR" +append_lm_eval_summary >/dev/null +''' + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "WORK_DIR": str(work_dir), + "EVAL_RESULT_DIR": str(results_dir), + "MODEL": "test-model", + "CONC": "7", + "KV_OFFLOADING": "none", + } + + subprocess.run(["bash", "-c", script], env=env, check=True) + + assert (work_dir / archive.name).read_bytes() == b"bfcl-archive" + assert not results_dir.exists() + + +def test_stage_eval_artifacts_copies_verifier_outputs_only(tmp_path: Path) -> None: + source_one = tmp_path / "source-one" + source_two = tmp_path / "source-two" + destination = tmp_path / "destination" + source_one.mkdir() + source_two.mkdir() + expected = { + "meta_env.json", + "results_bfcl.json", + "kimi_vendor_report.json", + "kimi_vendor_results.jsonl", + "bfcl_report.json", + "bfcl_upstream_artifacts.tar.gz", + "sample_eval.jsonl", + } + for filename in expected: + source = source_one if filename.endswith(".json") else source_two + (source / filename).write_text(filename) + (source_one / "unrelated.log").write_text("skip") + script = r''' +source "$BENCHMARK_LIB" +stage_eval_artifacts "$DESTINATION" "$SOURCE_ONE" "$SOURCE_TWO" +''' + subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "DESTINATION": str(destination), + "SOURCE_ONE": str(source_one), + "SOURCE_TWO": str(source_two), + "KV_OFFLOADING": "none", + }, + check=True, + ) + + assert {path.name for path in destination.iterdir()} == expected + + +def test_stage_eval_artifacts_propagates_copy_failure(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "bfcl_report.json").write_text("{}") + script = r''' +source "$BENCHMARK_LIB" +cp() { return 73; } +stage_eval_artifacts "$DESTINATION" "$SOURCE" +''' + + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "DESTINATION": str(tmp_path / "destination"), + "SOURCE": str(source), + "KV_OFFLOADING": "none", + }, + check=False, + ) + + assert result.returncode == 73 + + def test_summary_metadata_preserves_lm_eval_gsm8k_defaults(tmp_path: Path) -> None: meta = _summary_metadata(tmp_path) @@ -2170,6 +2287,9 @@ def test_fixed_eval_workflows_forward_provider_contract() -> None: assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" assert "*_vendor_report.json" in SINGLE_NODE_WORKFLOW.read_text() assert "bfcl_report.json" in SINGLE_NODE_WORKFLOW.read_text() + assert "bfcl_upstream_artifacts.tar.gz" in SINGLE_NODE_WORKFLOW.read_text() + assert "bfcl_vllm_minimax_m3" in SINGLE_NODE_WORKFLOW.read_text() + assert "bfcl_vllm_kimi" in SINGLE_NODE_WORKFLOW.read_text() @@ -2184,7 +2304,10 @@ def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" assert "*_vendor_report.json" in MULTINODE_WORKFLOW.read_text() assert "bfcl_report.json" in MULTINODE_WORKFLOW.read_text() + assert "bfcl_upstream_artifacts.tar.gz" in MULTINODE_WORKFLOW.read_text() + assert "bfcl_vllm_minimax_m3" in MULTINODE_WORKFLOW.read_text() + assert "bfcl_vllm_kimi" in MULTINODE_WORKFLOW.read_text() def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: @@ -2279,6 +2402,32 @@ def test_bfcl_rejects_unknown_suite() -> None: assert result.returncode == 2 assert "unsupported BFCL suite 'not_a_bfcl_suite'" in result.stderr +def test_bfcl_full_suite_thresholds_are_diagnostic_and_namespaced() -> None: + thresholds = yaml.safe_load( + (REPO_ROOT / "utils/evals/thresholds.yaml").read_text() + )["default"] + full_suite_tasks = ( + "bfcl_vllm_minimax_m3", + "bfcl_vllm_minimax_m3_simple_python", + "bfcl_vllm_minimax_m3_multiple", + "bfcl_vllm_minimax_m3_parallel", + "bfcl_vllm_minimax_m3_parallel_multiple", + "bfcl_vllm_kimi", + "bfcl_vllm_kimi_simple_python", + "bfcl_vllm_kimi_multiple", + "bfcl_vllm_kimi_parallel", + "bfcl_vllm_kimi_parallel_multiple", + "bfcl_vllm_kimi_multi_turn", + "bfcl_vllm_kimi_multi_turn_base", + "bfcl_vllm_kimi_multi_turn_miss_func", + "bfcl_vllm_kimi_multi_turn_miss_param", + "bfcl_vllm_kimi_multi_turn_long_context", + ) + + assert thresholds["bfcl_smoke"] == 0.75 + assert all(thresholds[task] == 0.0 for task in full_suite_tasks) + + def test_bfcl_dependency_timeout_uses_integration_error_and_stages( tmp_path: Path, ) -> None: @@ -2286,7 +2435,7 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( python_dir = tmp_path / "python" script = r""" source "$BENCHMARK_LIB" -unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE +export EVAL_SUITE=bfcl_vllm_kimi unset VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR selected_python() { printf 'ADAPTER_ARG=<%s>\n' "$@" @@ -2333,6 +2482,8 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( assert "ADAPTER_ARG=" in output assert f"ADAPTER_ARG=<{results_dir}>" in output assert "ADAPTER_ARG=<--integration-error>" in output + assert "ADAPTER_ARG=<--suite>" in output + assert "ADAPTER_ARG=" in output assert ( "ADAPTER_ARG=" in output ) @@ -2341,10 +2492,15 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( assert (results_dir / "results_bfcl.json").exists() assert "failed to write BFCL failure artifact" not in output assert "UNEXPECTED_SYSTEM_PYTHON" not in output + assert not (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() assert not python_dir.exists() def _run_bfcl_adapter_command( - tmp_path: Path, *, adapter_rc: int = 0 + tmp_path: Path, + *, + adapter_rc: int = 0, + suite: str = "", + archive_rc: int = 0, ) -> tuple[subprocess.CompletedProcess[str], tuple[Path, Path, Path, Path]]: results_dir = tmp_path / "results" runtime_dir = tmp_path / "runtime" @@ -2361,6 +2517,9 @@ def _run_bfcl_adapter_command( return 1 fi done + if [ "$TEST_ADAPTER_RC" -eq 0 ]; then + touch "$RESULTS_DIR/bfcl_report.json" "$RESULTS_DIR/results_bfcl.json" + fi return "$TEST_ADAPTER_RC" } _prepare_vendor_verifier_python() { @@ -2379,13 +2538,26 @@ def _run_bfcl_adapter_command( mkdir "$PROJECT_ROOT" printf '%s\n' "$PROJECT_ROOT" } +_archive_bfcl_upstream_artifacts() { + printf 'ARCHIVE_PROJECT_ROOT=<%s>\n' "$1" + printf 'ARCHIVE_PATH=<%s>\n' "$2" + if [ "$TEST_ARCHIVE_RC" -eq 0 ]; then + touch "$2" + fi + return "$TEST_ARCHIVE_RC" +} timeout() { printf 'TIMEOUT_ARG=<%s>\n' "$1" shift "$@" } append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR"; } -unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE EVAL_MAX_MODEL_LEN +if [ -n "$TEST_SUITE" ]; then + export EVAL_SUITE="$TEST_SUITE" +else + unset EVAL_SUITE +fi +unset EVAL_RESULT_DIR EVAL_COMPLETED_SUITE EVAL_MAX_MODEL_LEN compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } export EVAL_CONCURRENT_REQUESTS="" export EVAL_ONLY=false @@ -2408,6 +2580,8 @@ def _run_bfcl_adapter_command( "MODEL_NAME": "served-model", "OPENAI_API_KEY": "must-not-be-forwarded", "TEST_ADAPTER_RC": str(adapter_rc), + "TEST_SUITE": suite, + "TEST_ARCHIVE_RC": str(archive_rc), } for key in ( "EVAL_FRAMEWORK", @@ -2458,6 +2632,9 @@ def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( assert "PREPARE_ARG=" in output assert "PREPARE_ARG=<10>" in output assert "TIMEOUT_ARG=<900>" in output + assert "ADAPTER_ARG=<--suite>" not in output + assert "ARCHIVE_PROJECT_ROOT=" not in output + assert not (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() assert "ADAPTER_ARG=" not in output assert "UNEXPECTED_CONTEXT_LOAD" not in output assert f"STAGED=<{results_dir}>" not in output @@ -2469,6 +2646,63 @@ def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( assert not python_dir.exists() assert not project_root.exists() +def test_bfcl_full_suites_use_suite_specific_runtime_and_archive_before_cleanup( + tmp_path: Path, +) -> None: + suite_contracts = ( + ("bfcl_vllm_minimax_m3", "8"), + ("bfcl_vllm_kimi", "16"), + ) + + for suite, expected_threads in suite_contracts: + suite_tmp_path = tmp_path / suite + suite_tmp_path.mkdir() + result, paths = _run_bfcl_adapter_command( + suite_tmp_path, + suite=suite, + ) + results_dir, runtime_dir, python_dir, project_root = paths + output = result.stdout + result.stderr + + assert result.returncode == 0, result.stderr + assert "TIMEOUT_ARG=<7200>" in output + assert "ADAPTER_ARG=<--suite>" in output + assert f"ADAPTER_ARG=<{suite}>" in output + assert f"EVAL_COMPLETED_SUITE={suite}" in output + assert "ADAPTER_ARG=<--num-threads>" in output + assert f"ADAPTER_ARG=<{expected_threads}>" in output + assert f"ARCHIVE_PROJECT_ROOT=<{project_root}>" in output + assert ( + f"ARCHIVE_PATH=<{results_dir / 'bfcl_upstream_artifacts.tar.gz'}>" + in output + ) + assert (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() + assert not runtime_dir.exists() + assert not python_dir.exists() + assert not project_root.exists() + + +def test_bfcl_full_suite_archive_failure_preserves_scores_and_cleans_runtime( + tmp_path: Path, +) -> None: + result, paths = _run_bfcl_adapter_command( + tmp_path, + suite="bfcl_vllm_kimi", + archive_rc=73, + ) + results_dir, runtime_dir, python_dir, project_root = paths + output = result.stdout + result.stderr + + assert result.returncode == 73 + assert "failed to archive BFCL upstream artifacts (exit code 73)" in output + assert (results_dir / "bfcl_report.json").exists() + assert (results_dir / "results_bfcl.json").exists() + assert not (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() + assert not runtime_dir.exists() + assert not python_dir.exists() + assert not project_root.exists() + + def test_bfcl_adapter_timeout_writes_reports_stages_and_propagates( tmp_path: Path, ) -> None: @@ -2480,6 +2714,8 @@ def test_bfcl_adapter_timeout_writes_reports_stages_and_propagates( assert "EVAL_RC=124" in output assert output.count(f"STAGED=<{results_dir}>") == 1 assert "ADAPTER_ARG=<--integration-error>" in output + assert "ADAPTER_ARG=<--suite>" in output + assert "ADAPTER_ARG=" in output assert "ADAPTER_ARG=" in output assert (results_dir / "bfcl_report.json").exists() assert (results_dir / "results_bfcl.json").exists() @@ -2489,6 +2725,91 @@ def test_bfcl_adapter_timeout_writes_reports_stages_and_propagates( assert not python_dir.exists() assert not project_root.exists() +def test_bfcl_upstream_archive_is_deterministic_and_survives_cleanup( + tmp_path: Path, +) -> None: + project_root = tmp_path / "project" + result_path = project_root / "result/run/BFCL_v4_simple_python_result.json" + score_path = project_root / "score/run/BFCL_v4_simple_python_score.json" + id_path = project_root / "test_case_ids_to_generate.json" + for path, content in ( + (result_path, '{"id":"simple_python_0"}\n'), + (score_path, '{"accuracy":1.0}\n'), + (id_path, '{"simple_python":["simple_python_0"]}\n'), + ): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + first_archive = tmp_path / "first.tar.gz" + second_archive = tmp_path / "second.tar.gz" + script = r""" +source "$BENCHMARK_LIB" +VENDOR_VERIFIER_PYTHON="$PYTHON" +_archive_bfcl_upstream_artifacts "$PROJECT_ROOT" "$FIRST_ARCHIVE" +_archive_bfcl_upstream_artifacts "$PROJECT_ROOT" "$SECOND_ARCHIVE" +_cleanup_vendor_eval "$PROJECT_ROOT" +""" + subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "PYTHON": sys.executable, + "PROJECT_ROOT": str(project_root), + "FIRST_ARCHIVE": str(first_archive), + "SECOND_ARCHIVE": str(second_archive), + }, + text=True, + capture_output=True, + check=True, + ) + + assert first_archive.read_bytes() == second_archive.read_bytes() + with tarfile.open(first_archive, "r:gz") as archive: + assert archive.getnames() == [ + "result", + "result/run", + "result/run/BFCL_v4_simple_python_result.json", + "score", + "score/run", + "score/run/BFCL_v4_simple_python_score.json", + "test_case_ids_to_generate.json", + ] + assert not project_root.exists() + + +def test_bfcl_upstream_archive_rejects_symbolic_links(tmp_path: Path) -> None: + project_root = tmp_path / "project" + project_root.mkdir() + outside = tmp_path / "outside.json" + outside.write_text('{"secret":true}\n') + (project_root / "escape.json").symlink_to(outside) + archive = tmp_path / "unsafe.tar.gz" + script = r""" +source "$BENCHMARK_LIB" +VENDOR_VERIFIER_PYTHON="$PYTHON" +_archive_bfcl_upstream_artifacts "$PROJECT_ROOT" "$ARCHIVE" +""" + + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "PYTHON": sys.executable, + "PROJECT_ROOT": str(project_root), + "ARCHIVE": str(archive), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert "refusing to archive symbolic link: escape.json" in result.stderr + assert not archive.exists() + assert not (tmp_path / ".unsafe.tar.gz.tmp").exists() + + def test_bfcl_installer_uses_verified_wheel_in_selected_venv(tmp_path: Path) -> None: script = r""" source "$BENCHMARK_LIB" diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index d45c46e1db..4f770f2b87 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -10,6 +10,21 @@ "bfcl_multiple": 0.0, "bfcl_parallel": 0.0, "bfcl_irrelevance": 0.0, + "bfcl_vllm_minimax_m3": 0.0, + "bfcl_vllm_minimax_m3_simple_python": 0.0, + "bfcl_vllm_minimax_m3_multiple": 0.0, + "bfcl_vllm_minimax_m3_parallel": 0.0, + "bfcl_vllm_minimax_m3_parallel_multiple": 0.0, + "bfcl_vllm_kimi": 0.0, + "bfcl_vllm_kimi_simple_python": 0.0, + "bfcl_vllm_kimi_multiple": 0.0, + "bfcl_vllm_kimi_parallel": 0.0, + "bfcl_vllm_kimi_parallel_multiple": 0.0, + "bfcl_vllm_kimi_multi_turn": 0.0, + "bfcl_vllm_kimi_multi_turn_base": 0.0, + "bfcl_vllm_kimi_multi_turn_miss_func": 0.0, + "bfcl_vllm_kimi_multi_turn_miss_param": 0.0, + "bfcl_vllm_kimi_multi_turn_long_context": 0.0, "gpqa_diamond_cot_n_shot": 0.30, "swebench_lite": 0.50 }, From 72a8a6a1da269d2607a3fbc3cc7fb15beae67a6a Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:01:49 -0500 Subject: [PATCH 53/99] docs: document BFCL model quality suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:记录 BFCL 模型质量评估套件的选择方式、确定性约束、超时策略、阈值语义和调试产物。 --- utils/evals/EVALS.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 76e3955cde..8ec748a8e5 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -281,6 +281,47 @@ failed category does not become a second gate. BFCL reuses the existing eval job, upload paths, aggregation, and validation instead of adding a parallel workflow or artifact route. +#### BFCL V4 model-quality suites + +Two explicit BFCL suites extend the four-case endpoint smoke into broader +model-quality diagnostics: + +| Suite | Selected BFCL V4 categories | Requests | +|-------|-----------------------------|----------| +| `bfcl_vllm_minimax_m3` | `simple_python` (400), `multiple` (200), `parallel` (200), `parallel_multiple` (200) | 1000 | +| `bfcl_vllm_kimi` | The same 1000 single-turn cases plus 60 each from `multi_turn_base`, `multi_turn_miss_func`, `multi_turn_miss_param`, and `multi_turn_long_context` | 1240 | + +Select these suites explicitly with `eval-framework: bfcl`; `bfcl_smoke` +remains the framework default. Both suites use BFCL's OpenAI completions +handler against the local endpoint rather than a hosted-provider handler. They +fix temperature to `0.001`, disable request retries, and keep the 180-second +per-request timeout. MiniMax uses eight worker threads. Kimi uses 16 threads +and permits up to ten multi-turn steps. The whole-suite timeout is 7200 +seconds. + +The adapter builds a deterministic run-ID map from the pinned BFCL dataset. +Single-turn suites select every case in their named categories. The Kimi +multi-turn selection sorts each leaf category and takes its first 60 cases. +Although upstream BFCL evaluates these subsets with `partial_eval`, the +adapter rejects missing, unexpected, or duplicate result IDs and score headers +whose counts or accuracy do not reconcile with the selected corpus. + +The compatibility result publishes `bfcl_vllm_minimax_m3` or +`bfcl_vllm_kimi` as the aggregate task and preserves per-category +`bfcl_` tasks. Kimi also publishes a combined `bfcl_multi_turn` +task. Full-suite thresholds are `0.0`; they are diagnostic until repeated +hardware runs establish model, precision, and backend baselines. A completed +zero-score run therefore passes threshold validation, while dependency, +transport, timeout, malformed-output, and integration failures still fail. + +`bfcl_upstream_artifacts.tar.gz` preserves the pinned upstream result and +failure-only score JSONL files, exact selected-ID map, and file locks for +debugging. The native `bfcl_report.json` includes the package version, wheel +hash, source revision, integration revision, per-category score headers, +case IDs, failure records, and sampling settings. The compatibility +`results_bfcl.json` remains the only input to the normal InferenceX eval +collector and dashboard path. + ### Benchmark script flow All benchmark scripts in `benchmarks/` follow one of two flows: From 61e84e4430afc7e5003ebe894150da6640e1645f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:08:41 -0500 Subject: [PATCH 54/99] docs: clarify BFCL suite coverage limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:明确这些套件覆盖 BFCL V4 的模型特定非实时与多轮切片,不包含全部智能体类别。 --- utils/evals/EVALS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 8ec748a8e5..443fc2e418 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -291,6 +291,10 @@ model-quality diagnostics: | `bfcl_vllm_minimax_m3` | `simple_python` (400), `multiple` (200), `parallel` (200), `parallel_multiple` (200) | 1000 | | `bfcl_vllm_kimi` | The same 1000 single-turn cases plus 60 each from `multi_turn_base`, `multi_turn_miss_func`, `multi_turn_miss_param`, and `multi_turn_long_context` | 1240 | +These are the model-specific non-live and multi-turn slices used by the pinned +BFCL vLLM integration, not every BFCL V4 leaderboard category. They exclude +the V4 agentic web-search and memory evaluations. + Select these suites explicitly with `eval-framework: bfcl`; `bfcl_smoke` remains the framework default. Both suites use BFCL's OpenAI completions handler against the local endpoint rather than a hosted-provider handler. They From 1a8cfdca255cc175777f8f4a79296f495de77775 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:17:46 -0500 Subject: [PATCH 55/99] feat: preserve BFCL license with artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在 BFCL 上游调试产物中保留 Apache 2.0 许可证与可机读来源清单,并补充回归测试和文档。 --- utils/evals/EVALS.md | 7 ++++--- utils/evals/bfcl_eval.py | 39 +++++++++++++++++++++++++++++++++++ utils/evals/test_bfcl_eval.py | 29 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 443fc2e418..784bd39579 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -319,9 +319,10 @@ zero-score run therefore passes threshold validation, while dependency, transport, timeout, malformed-output, and integration failures still fail. `bfcl_upstream_artifacts.tar.gz` preserves the pinned upstream result and -failure-only score JSONL files, exact selected-ID map, and file locks for -debugging. The native `bfcl_report.json` includes the package version, wheel -hash, source revision, integration revision, per-category score headers, +failure-only score JSONL files, exact selected-ID map, file locks, provenance +manifest, and Apache 2.0 license copy for debugging and attribution. +The native `bfcl_report.json` includes the package version, wheel hash, source +revision, integration revision, per-category score headers, case IDs, failure records, and sampling settings. The compatibility `results_bfcl.json` remains the only input to the normal InferenceX eval collector and dashboard path. diff --git a/utils/evals/bfcl_eval.py b/utils/evals/bfcl_eval.py index 15b10cc243..d649edf7f2 100755 --- a/utils/evals/bfcl_eval.py +++ b/utils/evals/bfcl_eval.py @@ -36,6 +36,12 @@ UPSTREAM_REF = f"{BFCL_PACKAGE}=={BFCL_PACKAGE_VERSION}" SOURCE_REVISION = "6ea57973c7a6097fd7c5915698c54c17c5b1b6c8" VLLM_INTEGRATION_REF = "7ecb11405df86b202f4c5cca322bd133052fee82" +UPSTREAM_LICENSE = "Apache-2.0" +UPSTREAM_LICENSE_URL = ( + f"{UPSTREAM_REPOSITORY}/blob/{SOURCE_REVISION}/LICENSE" +) +UPSTREAM_LICENSE_FILENAME = "BFCL_LICENSE.apache-2.0.txt" +UPSTREAM_ATTRIBUTION_FILENAME = "BFCL_ATTRIBUTION.json" # Dict insertion order is intentional: smoke reports and the upstream run-ID # file remain byte-for-byte stable. @@ -402,6 +408,38 @@ def _write_json(path: Path, value: Mapping[str, Any]) -> None: ) +def _write_upstream_attribution(project_root: Path) -> None: + """Keep BFCL provenance and its Apache license with archived outputs.""" + project_root.mkdir(parents=True, exist_ok=True) + repository_license = Path(__file__).resolve().parents[2] / "LICENSE" + if not repository_license.is_file(): + raise FileNotFoundError(f"Apache license file not found: {repository_license}") + (project_root / UPSTREAM_LICENSE_FILENAME).write_bytes( + repository_license.read_bytes() + ) + _write_json( + project_root / UPSTREAM_ATTRIBUTION_FILENAME, + { + "artifact": "BFCL-generated evaluation results", + "upstream": { + "package": BFCL_PACKAGE, + "package_version": BFCL_PACKAGE_VERSION, + "wheel_sha256": BFCL_WHEEL_SHA256, + "repository": UPSTREAM_REPOSITORY, + "source_revision": SOURCE_REVISION, + "vllm_integration_revision": VLLM_INTEGRATION_REF, + "license": UPSTREAM_LICENSE, + "license_url": UPSTREAM_LICENSE_URL, + "license_file": UPSTREAM_LICENSE_FILENAME, + }, + "modifications": ( + "InferenceX selected deterministic case subsets and projected " + "upstream scores; this archive does not modify upstream BFCL source." + ), + }, + ) + + def _prepare_output_paths(output_dir: Path) -> tuple[Path, Path]: output_dir.mkdir(parents=True, exist_ok=True) native_path = output_dir / NATIVE_REPORT_FILENAME @@ -901,6 +939,7 @@ def run_evaluation( raise TypeError("upstream_runner must be callable") os.environ["BFCL_PROJECT_ROOT"] = str(bfcl_project_root) + _write_upstream_attribution(bfcl_project_root) selected_case_ids = _build_suite_case_ids(suite) _write_id_map(bfcl_project_root, selected_case_ids) upstream_runner( diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index 24163fd087..4a3cb00dac 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -328,6 +328,35 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( "parallel": ["parallel_1"], "irrelevance": ["irrelevance_0"], } + assert (project_root / be.UPSTREAM_LICENSE_FILENAME).read_text( + encoding="utf-8" + ).startswith(" Apache License") + attribution = json.loads( + (project_root / be.UPSTREAM_ATTRIBUTION_FILENAME).read_text( + encoding="utf-8" + ) + ) + assert attribution == { + "artifact": "BFCL-generated evaluation results", + "upstream": { + "package": "bfcl-eval", + "package_version": "2026.3.23", + "wheel_sha256": be.BFCL_WHEEL_SHA256, + "repository": "https://github.com/ShishirPatil/gorilla", + "source_revision": be.SOURCE_REVISION, + "vllm_integration_revision": be.VLLM_INTEGRATION_REF, + "license": "Apache-2.0", + "license_url": ( + "https://github.com/ShishirPatil/gorilla/blob/" + f"{be.SOURCE_REVISION}/LICENSE" + ), + "license_file": be.UPSTREAM_LICENSE_FILENAME, + }, + "modifications": ( + "InferenceX selected deterministic case subsets and projected upstream " + "scores; this archive does not modify upstream BFCL source." + ), + } compatibility = _compatibility(output_dir) native = _native(output_dir) From 7b616032230aef184f1c1c8cfb1b550f95c67af1 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:47:57 -0500 Subject: [PATCH 56/99] feat: complete tool-use eval integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:完成工具调用评估集成,并强化多后端分发、产物处理与失败语义。 --- .../workflows/benchmark-multinode-tmpl.yml | 25 +- .github/workflows/benchmark-tmpl.yml | 23 +- benchmarks/benchmark_lib.sh | 175 +++++++++--- benchmarks/multi_node/agentic_srt.sh | 5 +- benchmarks/multi_node/llm-d/job.slurm | 11 +- benchmarks/multi_node/llm-d/submit.sh | 5 + .../agentic/agg-gb300-tp4-mtp-kvoffload.yaml | 3 + ...-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml | 3 + ...gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml | 3 + ...00-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml | 3 + ...00-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml | 3 + ...00-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml | 3 + ...p2-c72-mtp-hicache-session-jid2527415.yaml | 3 + ...4-c128-mtp-hicache-session-jid2527417.yaml | 3 + ...p4-c16-mtp-hicache-session-jid2530027.yaml | 3 + ...p4-c32-mtp-hicache-session-jid2530028.yaml | 3 + ...p4-c64-mtp-hicache-session-jid2530029.yaml | 3 + ...tp4-c8-mtp-hicache-session-jid2530030.yaml | 3 + ...p4-c96-mtp-hicache-session-jid2527409.yaml | 3 + .../agentic/minimaxm3_fp4_mi355x_mtp.sh | 1 - .../agentic/minimaxm3_fp8_mi300x_mtp.sh | 1 - .../agentic/minimaxm3_fp8_mi325x_mtp.sh | 1 - .../agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh | 1 - .../agentic/qwen3.5_fp8_mi300x_mtp.sh | 1 - .../agentic/qwen3.5_fp8_mi325x_mtp.sh | 1 - runners/launch_h100-cr.sh | 2 +- runners/launch_mi325x-tw.sh | 2 +- runners/patch_srt_eval_dispatch.py | 9 +- runners/test_slurm_utils.py | 63 ++++- utils/evals/EVALS.md | 68 ++++- utils/evals/{bfcl_eval.py => bfcl_adapter.py} | 81 +++--- utils/evals/kimi_vendor_eval.py | 18 +- utils/evals/minimax_m3_full_eval.py | 60 ++-- utils/evals/minimax_provider_eval.py | 261 ++++-------------- utils/evals/test_batched_eval.py | 6 +- utils/evals/test_bfcl_eval.py | 49 +++- utils/evals/test_kimi_vendor_eval.py | 25 +- utils/evals/test_minimax_m3_full_eval.py | 48 ++++ utils/evals/test_minimax_provider_eval.py | 70 ++--- utils/evals/test_run_eval_dispatch.py | 236 ++++++++++++++-- utils/evals/validate_scores.py | 26 +- 41 files changed, 853 insertions(+), 460 deletions(-) rename utils/evals/{bfcl_eval.py => bfcl_adapter.py} (95%) mode change 100755 => 100644 diff --git a/.github/workflows/benchmark-multinode-tmpl.yml b/.github/workflows/benchmark-multinode-tmpl.yml index 970417a9da..86fc6be462 100644 --- a/.github/workflows/benchmark-multinode-tmpl.yml +++ b/.github/workflows/benchmark-multinode-tmpl.yml @@ -363,6 +363,9 @@ jobs: fi # Export RESULT_FILENAME early so it's available for artifact uploads even if cancelled echo "RESULT_FILENAME=${RESULT_FILENAME}" >> "$GITHUB_ENV" + echo "EVAL_ARTIFACT_RECIPE=${RECIPE_FINGERPRINT:0:16}" >> "$GITHUB_ENV" + eval_artifact_conc="$(python3 -c 'import hashlib,os; print(hashlib.sha256(os.environ["CONC_LIST"].encode()).hexdigest()[:12])')" + echo "EVAL_ARTIFACT_CONC=${eval_artifact_conc}" >> "$GITHUB_ENV" export ${{ join(fromJson(inputs.prefill-additional-settings), ' ') }} ${{ join(fromJson(inputs.decode-additional-settings), ' ') }} export IS_MULTINODE=true @@ -488,18 +491,17 @@ jobs: if: ${{ always() && (env.RUN_EVAL == 'true' || inputs.eval-only) }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: eval_${{ env.EXP_NAME }}_${{ env.PRECISION }}_${{ env.FRAMEWORK }}_p${{ env.PREFILL_NUM_WORKERS }}x${{ env.PREFILL_TP }}p${{ env.PREFILL_PP_SIZE }}c${{ env.PREFILL_DCP_SIZE }}k${{ env.PREFILL_PCP_SIZE }}e${{ env.PREFILL_EP }}d${{ env.PREFILL_DP_ATTN }}_d${{ env.DECODE_NUM_WORKERS }}x${{ env.DECODE_TP }}p${{ env.DECODE_PP_SIZE }}c${{ env.DECODE_DCP_SIZE }}k${{ env.DECODE_PCP_SIZE }}e${{ env.DECODE_EP }}d${{ env.DECODE_DP_ATTN }}_kv${{ env.KV_OFFLOADING }}-${{ env.KV_OFFLOAD_BACKEND }}_spec${{ env.SPEC_DECODING }}_c${{ join(fromJson(inputs.conc-list), 'x') }}_${{ runner.name }} + name: eval_${{ env.EXP_NAME }}_${{ env.PRECISION }}_${{ env.FRAMEWORK }}_p${{ env.PREFILL_NUM_WORKERS }}x${{ env.PREFILL_TP }}p${{ env.PREFILL_PP_SIZE }}c${{ env.PREFILL_DCP_SIZE }}k${{ env.PREFILL_PCP_SIZE }}e${{ env.PREFILL_EP }}d${{ env.PREFILL_DP_ATTN }}_d${{ env.DECODE_NUM_WORKERS }}x${{ env.DECODE_TP }}p${{ env.DECODE_PP_SIZE }}c${{ env.DECODE_DCP_SIZE }}k${{ env.DECODE_PCP_SIZE }}e${{ env.DECODE_EP }}d${{ env.DECODE_DP_ATTN }}_g${{ env.DISAGG }}_r${{ env.EVAL_ARTIFACT_RECIPE }}_c${{ env.EVAL_ARTIFACT_CONC }}_kv${{ env.KV_OFFLOADING }}-${{ env.KV_OFFLOAD_BACKEND }}_spec${{ env.SPEC_DECODING }}_${{ env.EVAL_FRAMEWORK }}_${{ env.EVAL_SUITE }}_${{ runner.name }}_${{ github.run_attempt }} path: | meta_env.json results*.json - *_vendor_report.json - *_vendor_results.jsonl + *_report.json + *_results.jsonl sample*.jsonl agent_preds.json predictions.jsonl swebench_report_*.json - bfcl_report.json - bfcl_upstream_artifacts.tar.gz + *_artifacts.tar.gz *.traj* if-no-files-found: ${{ inputs.eval-only && 'error' || 'ignore' }} @@ -510,21 +512,16 @@ jobs: if [[ -z "${expected_concs}" ]]; then expected_concs="$(printf '%s\n' "${CONC_LIST}" | tr ' ' '\n' | sort -n | tail -1)" fi - if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then - python3 utils/evals/validate_scores.py --metric-prefix 'acc,' --expected-concs "${expected_concs}" - else - python3 utils/evals/validate_scores.py --expected-concs "${expected_concs}" - fi + python3 utils/evals/validate_scores.py --expected-concs "${expected_concs}" - name: Cleanup eval outputs (post-upload) if: ${{ always() && (inputs.run-eval || inputs.eval-only) }} run: | rm -f meta_env.json || true rm -f results*.json || true - rm -f -- ./*_vendor_report.json || true - rm -f -- ./*_vendor_results.jsonl || true - rm -f bfcl_report.json || true - rm -f bfcl_upstream_artifacts.tar.gz || true + rm -f -- ./*_report.json || true + rm -f -- ./*_results.jsonl || true + rm -f -- ./*_artifacts.tar.gz || true rm -f sample*.jsonl || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 4853a9b96a..1b3fc3fbd5 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -409,14 +409,13 @@ jobs: if: ${{ always() && (env.RUN_EVAL == 'true' || inputs.eval-only) }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: eval_${{ env.EXP_NAME }}_${{ env.RESULT_FILENAME }} + name: eval_${{ env.RESULT_FILENAME }}_${{ env.EVAL_FRAMEWORK }}_${{ env.EVAL_SUITE }}_${{ github.run_attempt }} path: | meta_env.json results*.json - *_vendor_report.json - *_vendor_results.jsonl - bfcl_report.json - bfcl_upstream_artifacts.tar.gz + *_report.json + *_results.jsonl + *_artifacts.tar.gz sample*.jsonl agent_preds.json predictions.jsonl @@ -426,12 +425,7 @@ jobs: - name: Verify eval scores if: ${{ (success() || failure()) && inputs.eval-only }} - run: | - if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then - python3 utils/evals/validate_scores.py --metric-prefix 'acc,' - else - python3 utils/evals/validate_scores.py - fi + run: python3 utils/evals/validate_scores.py - name: Cleanup eval outputs (post-upload) if: ${{ always() && (env.RUN_EVAL == 'true' || inputs.eval-only) }} @@ -439,11 +433,10 @@ jobs: rm -f meta_env.json || true # Remove any eval results JSONs that were moved into workspace rm -f results*.json || true - rm -f -- ./*_vendor_report.json || true - rm -f -- ./*_vendor_results.jsonl || true + rm -f -- ./*_report.json || true + rm -f -- ./*_results.jsonl || true rm -f sample*.jsonl || true - rm -f bfcl_report.json || true - rm -f bfcl_upstream_artifacts.tar.gz || true + rm -f -- ./*_artifacts.tar.gz || true rm -f agent_preds.json predictions.jsonl swebench_report_*.json *.traj* || true - name: Resource cleanup (post-run) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index b00fe3c7f2..3501b33ae3 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1237,6 +1237,59 @@ _cleanup_vendor_eval() { done } +_has_eval_result() { + local results_dir="$1" + local filename_prefix="$2" + local matches=("${results_dir}/${filename_prefix}"*.json) + [ -f "${matches[0]}" ] +} + +_prepare_eval_artifact_family() { + local results_dir="$1" + local family="$2" + local artifact rm_rc=0 + local artifacts=() + + export EVAL_RESULT_DIR="" + case "$family" in + kimi) + artifacts=( + "${results_dir}"/results_kimi_vendor_*.json + "${results_dir}/kimi_vendor_report.json" + ) + ;; + minimax) + artifacts=( + "${results_dir}"/results_minimax_vendor_*.json + "${results_dir}/minimax_vendor_report.json" + "${results_dir}/minimax_vendor_results.jsonl" + ) + ;; + bfcl) + artifacts=( + "${results_dir}"/results_bfcl*.json + "${results_dir}/bfcl_report.json" + "${results_dir}/bfcl_upstream_artifacts.tar.gz" + ) + ;; + *) + echo "ERROR: unsupported eval artifact family '${family}'" >&2 + return 2 + ;; + esac + + for artifact in "${artifacts[@]}"; do + if [ -e "$artifact" ] || [ -L "$artifact" ]; then + rm -f -- "$artifact" || rm_rc=$? + if [ "$rm_rc" -ne 0 ]; then + echo "ERROR: failed to remove stale eval artifact ${artifact}" >&2 + return "$rm_rc" + fi + fi + done + export EVAL_RESULT_DIR="$results_dir" +} + _write_kimi_vendor_integration_error() { local adapter_path="$1" local model_name="$2" @@ -1244,7 +1297,7 @@ _write_kimi_vendor_integration_error() { local task_name="$4" local message="$5" - python3 "$adapter_path" \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ --model "$model_name" \ --output-dir "$results_dir" \ --task-name "$task_name" \ @@ -1290,7 +1343,7 @@ _run_kimi_tool_call_schema_eval() { mkdir -p "$results_dir" || return $? results_dir="$(cd "$results_dir" && pwd)" || return $? - export EVAL_RESULT_DIR="$results_dir" + _prepare_eval_artifact_family "$results_dir" kimi || return $? local setup_rc=0 integration_error="" _prepare_vendor_verifier_python "Kimi-Vendor-Verifier" "kimi-vendor-python" || { @@ -1312,8 +1365,6 @@ _run_kimi_tool_call_schema_eval() { } fi if [ "$setup_rc" -ne 0 ]; then - _cleanup_vendor_eval \ - "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" echo "ERROR: ${integration_error}" >&2 local artifact_rc=0 _write_kimi_vendor_integration_error \ @@ -1322,6 +1373,8 @@ _run_kimi_tool_call_schema_eval() { if [ "$artifact_rc" -ne 0 ]; then echo "ERROR: failed to write Kimi verifier failure artifact (exit code ${artifact_rc})" >&2 fi + _cleanup_vendor_eval \ + "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$setup_rc" fi @@ -1337,6 +1390,17 @@ _run_kimi_tool_call_schema_eval() { --task-name "$eval_suite" \ --timeout-seconds "$timeout_seconds" \ || eval_rc=$? + if [ "$eval_rc" -ne 0 ] \ + && ! _has_eval_result "$results_dir" "results_kimi_vendor_"; then + integration_error="Kimi Vendor Verifier failed with exit code ${eval_rc}" + local artifact_rc=0 + _write_kimi_vendor_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$eval_suite" \ + "$integration_error" || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write Kimi verifier failure artifact (exit code ${artifact_rc})" >&2 + fi + fi _cleanup_vendor_eval \ "$runtime_dir" "$checkout_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$eval_rc" @@ -1528,16 +1592,13 @@ _run_bfcl_suite_eval() { fi local model_name="${MODEL_NAME:-${MODEL:-}}" - local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/bfcl_eval.py" + local adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/bfcl_adapter.py" local runtime_dir="" local project_root="" mkdir -p "$results_dir" || return $? results_dir="$(cd "$results_dir" && pwd)" || return $? - export EVAL_RESULT_DIR="$results_dir" - if [ "$archive_upstream" = true ]; then - rm -f "${results_dir}/bfcl_upstream_artifacts.tar.gz" - fi + _prepare_eval_artifact_family "$results_dir" bfcl || return $? local setup_rc=0 integration_error="" _prepare_vendor_verifier_python "BFCL" "bfcl-python" true 10 || { @@ -1708,7 +1769,7 @@ _run_minimax_m3_smoke_eval() { mkdir -p "$results_dir" || return $? results_dir="$(cd "$results_dir" && pwd)" || return $? - export EVAL_RESULT_DIR="$results_dir" + _prepare_eval_artifact_family "$results_dir" minimax || return $? local setup_rc=0 integration_error="" _prepare_vendor_verifier_python "MiniMax Provider Verifier" "minimax-vendor-python" || { @@ -1722,8 +1783,6 @@ _run_minimax_m3_smoke_eval() { } fi if [ "$setup_rc" -ne 0 ]; then - _cleanup_vendor_eval \ - "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" echo "ERROR: ${integration_error}" >&2 local artifact_rc=0 _write_minimax_vendor_integration_error \ @@ -1732,6 +1791,8 @@ _run_minimax_m3_smoke_eval() { if [ "$artifact_rc" -ne 0 ]; then echo "ERROR: failed to write MiniMax verifier failure artifact (exit code ${artifact_rc})" >&2 fi + _cleanup_vendor_eval \ + "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$setup_rc" fi @@ -1746,6 +1807,17 @@ _run_minimax_m3_smoke_eval() { --request-timeout-seconds 180 \ --timeout-seconds 900 \ || eval_rc=$? + if [ "$eval_rc" -ne 0 ] \ + && ! _has_eval_result "$results_dir" "results_minimax_vendor_"; then + integration_error="MiniMax Provider Verifier failed with exit code ${eval_rc}" + local artifact_rc=0 + _write_minimax_vendor_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write MiniMax verifier failure artifact (exit code ${artifact_rc})" >&2 + fi + fi _cleanup_vendor_eval \ "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$eval_rc" @@ -1778,6 +1850,18 @@ _prepare_minimax_m3_full_runtime() { printf '%s\n' "$runtime_dir" } +_write_minimax_m3_full_integration_error() { + local adapter_path="$1" + local model_name="$2" + local results_dir="$3" + local message="$4" + + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" failure \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --message "$message" +} + _run_minimax_m3_full_eval() { local port="${PORT:-8888}" local results_dir="${EVAL_RESULT_DIR:-}" @@ -1812,7 +1896,7 @@ _run_minimax_m3_full_eval() { mkdir -p "$results_dir" || return $? results_dir="$(cd "$results_dir" && pwd)" || return $? - export EVAL_RESULT_DIR="$results_dir" + _prepare_eval_artifact_family "$results_dir" minimax || return $? local setup_rc=0 integration_error="" _prepare_vendor_verifier_python "MiniMax M3 full verifier" "minimax-m3-full-python" || { @@ -1827,10 +1911,13 @@ _run_minimax_m3_full_eval() { fi if [ "$setup_rc" -ne 0 ]; then echo "ERROR: ${integration_error}" >&2 - "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" failure \ - --model "$model_name" \ - --output-dir "$results_dir" \ - --message "$integration_error" || true + local artifact_rc=0 + _write_minimax_m3_full_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write MiniMax full verifier failure artifact (exit code ${artifact_rc})" >&2 + fi _cleanup_vendor_eval \ "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$setup_rc" @@ -1845,6 +1932,17 @@ _run_minimax_m3_full_eval() { --model "$model_name" \ --output-dir "$results_dir" \ || eval_rc=$? + if [ "$eval_rc" -ne 0 ] \ + && ! _has_eval_result "$results_dir" "results_minimax_vendor_full_"; then + integration_error="MiniMax M3 full verifier failed with exit code ${eval_rc}" + local artifact_rc=0 + _write_minimax_m3_full_integration_error \ + "$adapter_path" "$model_name" "$results_dir" "$integration_error" \ + || artifact_rc=$? + if [ "$artifact_rc" -ne 0 ]; then + echo "ERROR: failed to write MiniMax full verifier failure artifact (exit code ${artifact_rc})" >&2 + fi + fi _cleanup_vendor_eval \ "$runtime_dir" "${VENDOR_VERIFIER_PYTHON_CLEANUP_DIR:-}" return "$eval_rc" @@ -2296,30 +2394,15 @@ append_lm_eval_summary() { return 0 fi - # Move eval artifacts into PWD (no new directories in workspace) - if [ -f "${meta_json}" ]; then - mv -f "${meta_json}" ./ || echo "WARN: failed to move ${meta_json}" >&2 - fi - if [ -d "${out_dir}" ]; then - while IFS= read -r -d '' jf; do - base=$(basename "$jf") - if [ "$base" != "meta_env.json" ]; then - mv -f "$jf" ./ || echo "WARN: failed to move ${jf}" >&2 - fi - done < <(find "${out_dir}" -type f -name "*.json*" -print0 2>/dev/null) - fi - if [ -f "${out_dir}/bfcl_upstream_artifacts.tar.gz" ] \ - && ! mv -f "${out_dir}/bfcl_upstream_artifacts.tar.gz" ./; then - echo "ERROR: failed to move ${out_dir}/bfcl_upstream_artifacts.tar.gz" >&2 - return 1 - fi + # Copy the complete allowlisted eval artifact set before removing its temp dir. + stage_eval_artifacts "$(pwd)" "$out_dir" || return $? # Best-effort cleanup of the temp directory if [ -n "${out_dir}" ] && [ -d "${out_dir}" ]; then rm -rf --one-file-system "${out_dir}" || rm -rf "${out_dir}" || true fi - echo "Moved eval artifacts to: $(pwd)" + echo "Staged eval artifacts in: $(pwd)" } stage_eval_artifacts() { @@ -2328,23 +2411,32 @@ stage_eval_artifacts() { mkdir -p "$destination" || return $? local source_dir artifact + local copied=0 local artifacts=() for source_dir in "$@"; do [ -d "$source_dir" ] || continue artifacts=( "$source_dir"/meta_env.json "$source_dir"/results*.json - "$source_dir"/*_vendor_report.json - "$source_dir"/*_vendor_results.jsonl - "$source_dir"/bfcl_report.json - "$source_dir"/bfcl_upstream_artifacts.tar.gz + "$source_dir"/*_report.json + "$source_dir"/*_results.jsonl + "$source_dir"/*_artifacts.tar.gz "$source_dir"/sample*.jsonl + "$source_dir"/agent_preds.json + "$source_dir"/swebench_report_*.json + "$source_dir"/predictions.jsonl + "$source_dir"/*.traj* ) for artifact in "${artifacts[@]}"; do [ -f "$artifact" ] || continue cp -f "$artifact" "$destination/" || return $? + copied=$((copied + 1)) done done + if [ "$copied" -eq 0 ]; then + echo "ERROR: no eval artifacts found to stage" >&2 + return 1 + fi } @@ -2835,8 +2927,8 @@ run_eval() { fi local stage_rc=0 - # Agentic eval-only recipes have no separate staging step. Verifier failures - # also carry diagnostic score artifacts to preserve. + # Agentic eval-only recipes have no separate staging step. Provider + # failures are staged before returning so diagnostic artifacts survive. if { [ "${EVAL_ONLY:-false}" = "true" ] && [ "$scenario_is_agentic" = "1" ]; } \ || { { [ "$framework" = "kimi-vendor" ] \ || [ "$framework" = "minimax-vendor" ] \ @@ -2844,7 +2936,6 @@ run_eval() { && [ "$eval_rc" -ne 0 ]; }; then append_lm_eval_summary || stage_rc=$? fi - if [ "$eval_rc" -ne 0 ]; then echo "ERROR: run_eval failed with exit code $eval_rc" >&2 if [ "${EVAL_ONLY:-false}" = "true" ]; then diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index dea0881327..79a36da524 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -26,6 +26,8 @@ for concurrency in "${CONCURRENCIES[@]}"; do fi done +resolve_trace_source +install_agentic_deps wait_for_agentic_servers_idle() { local timeout_seconds="${AIPERF_DRAIN_TIMEOUT_SECONDS:-1800}" @@ -96,9 +98,6 @@ raise SystemExit(f"Agentic servers did not drain within {timeout_seconds} second PY } -resolve_trace_source -install_agentic_deps - # The AgentX scenario's first-turn cache-bust marker includes AIPerf's unique # per-invocation benchmark ID. Each point therefore gets a disjoint KV keyspace # while its own warmup and profile phases share markers. This makes sequential diff --git a/benchmarks/multi_node/llm-d/job.slurm b/benchmarks/multi_node/llm-d/job.slurm index 8d3a597731..536507bee5 100644 --- a/benchmarks/multi_node/llm-d/job.slurm +++ b/benchmarks/multi_node/llm-d/job.slurm @@ -154,6 +154,11 @@ exec docker run --rm \ -e EVAL_LIMIT=$EVAL_LIMIT \ -e EVAL_SUITE=$EVAL_SUITE \ -e SWEBENCH_GEN_MODE=$SWEBENCH_GEN_MODE \ + -e SWEBENCH_USE_MODAL=$SWEBENCH_USE_MODAL \ + -e MODAL_TOKEN_ID \ + -e MODAL_TOKEN_SECRET \ + -e IS_AGENTIC=$IS_AGENTIC \ + -e SCENARIO_TYPE=$SCENARIO_TYPE \ -e FRAMEWORK=$FRAMEWORK \ -e PRECISION=$PRECISION \ -e MODEL_PREFIX=$MODEL_PREFIX \ @@ -191,7 +196,8 @@ elif [[ "$LLMD_CONTAINER_ENGINE" == "pyxis" ]]; then export BENCH_INPUT_LEN BENCH_OUTPUT_LEN BENCH_MAX_CONCURRENCY export BENCH_REQUEST_RATE BENCH_RANDOM_RANGE_RATIO BENCH_NUM_PROMPTS_MULTIPLIER export RUN_EVAL EVAL_ONLY EVAL_CONC EVAL_FRAMEWORK EVAL_LIMIT EVAL_SUITE - export SWEBENCH_GEN_MODE FRAMEWORK PRECISION MODEL_PREFIX + export SWEBENCH_GEN_MODE SWEBENCH_USE_MODAL MODAL_TOKEN_ID MODAL_TOKEN_SECRET + export IS_AGENTIC SCENARIO_TYPE FRAMEWORK PRECISION MODEL_PREFIX export RUNNER_TYPE RESULT_FILENAME SPEC_DECODING IS_MULTINODE CONFIG_FILE PYXIS_ENV_LIST="NUM_NODES,PREFILL_NODES,DECODE_NODES,ALL_IPS,PREFILL_LEADER_IP,DECODE_LEADER_IP" @@ -201,7 +207,8 @@ elif [[ "$LLMD_CONTAINER_ENGINE" == "pyxis" ]]; then PYXIS_ENV_LIST+=",BENCH_INPUT_LEN,BENCH_OUTPUT_LEN,BENCH_MAX_CONCURRENCY" PYXIS_ENV_LIST+=",BENCH_REQUEST_RATE,BENCH_RANDOM_RANGE_RATIO,BENCH_NUM_PROMPTS_MULTIPLIER" PYXIS_ENV_LIST+=",RUN_EVAL,EVAL_ONLY,EVAL_CONC,EVAL_FRAMEWORK,EVAL_LIMIT,EVAL_SUITE" - PYXIS_ENV_LIST+=",SWEBENCH_GEN_MODE,FRAMEWORK,PRECISION,MODEL_PREFIX" + PYXIS_ENV_LIST+=",SWEBENCH_GEN_MODE,SWEBENCH_USE_MODAL,MODAL_TOKEN_ID,MODAL_TOKEN_SECRET" + PYXIS_ENV_LIST+=",IS_AGENTIC,SCENARIO_TYPE,FRAMEWORK,PRECISION,MODEL_PREFIX" PYXIS_ENV_LIST+=",RUNNER_TYPE,RESULT_FILENAME,SPEC_DECODING,IS_MULTINODE,CONFIG_FILE" PYXIS_MOUNTS="${MODEL_DIR}:/models:ro" diff --git a/benchmarks/multi_node/llm-d/submit.sh b/benchmarks/multi_node/llm-d/submit.sh index 3781a52382..9d00125c1c 100755 --- a/benchmarks/multi_node/llm-d/submit.sh +++ b/benchmarks/multi_node/llm-d/submit.sh @@ -83,6 +83,11 @@ export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" export EVAL_LIMIT="${EVAL_LIMIT:-}" export EVAL_SUITE="${EVAL_SUITE:-}" export SWEBENCH_GEN_MODE="${SWEBENCH_GEN_MODE:-}" +export SWEBENCH_USE_MODAL="${SWEBENCH_USE_MODAL:-false}" +export MODAL_TOKEN_ID="${MODAL_TOKEN_ID:-}" +export MODAL_TOKEN_SECRET="${MODAL_TOKEN_SECRET:-}" +export IS_AGENTIC="${IS_AGENTIC:-0}" +export SCENARIO_TYPE="${SCENARIO_TYPE:-}" export FRAMEWORK="${FRAMEWORK:-llmd-vllm}" export PRECISION="${PRECISION:-}" export MODEL_PREFIX="${MODEL_PREFIX:-}" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml index 904bed7305..6825b4868c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-kvoffload.yaml @@ -54,6 +54,9 @@ frontend: # the system env. Applies to both dynamo.hash (source) and dynamo.wheel. PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml index 56e1375365..821a2eb560 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-12p4d-dep8-dep16-c1536-mtp-kvoffload.yaml @@ -50,6 +50,9 @@ frontend: DYN_TCP_REQUEST_TIMEOUT: "60" PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml index 35fb27f95f..378dfdb9e7 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml @@ -47,6 +47,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml index 00842b8bef..0e649560c1 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml @@ -47,6 +47,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml index 8903781a74..11cb22fed8 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml @@ -46,6 +46,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml index 7ceb760b0c..ae8d4760d8 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml @@ -47,6 +47,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: + dyn-chat-processor: sglang + tool-call-parser: deepseekv4 + reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml index c6f0067638..105594d9cc 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml index b547225dbb..c331e02c17 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml index f45ff6c0e8..ddb0a449fa 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml index 36c04783f2..96bb5bd94a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml index c9fc5a7008..9ca1be72ad 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml index 0336533759..594728bf54 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml index 7fbbc63574..5be3b9f497 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml @@ -31,6 +31,9 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: + dyn-chat-processor: sglang + tool-call-parser: qwen3_coder + reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh index c1e3c924d7..11135a0583 100644 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh @@ -11,7 +11,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION diff --git a/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh index 08a4da1aa8..4234a66490 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp8_mi300x_mtp.sh @@ -6,7 +6,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION PORT EVAL_ONLY diff --git a/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh index 1002073712..28d901417b 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp8_mi325x_mtp.sh @@ -4,7 +4,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING RESULT_DIR DURATION EP_SIZE DP_ATTENTION PORT EVAL_ONLY diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh index 7f229cfae4..22de10579f 100644 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh @@ -8,7 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh index 4bda81c58a..faffe09721 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh @@ -6,7 +6,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh index d9601bdd4d..5f4180dcaf 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh @@ -8,7 +8,6 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars \ MODEL TP CONC EP_SIZE \ diff --git a/runners/launch_h100-cr.sh b/runners/launch_h100-cr.sh index f111a0dfcb..07bfa80dda 100644 --- a/runners/launch_h100-cr.sh +++ b/runners/launch_h100-cr.sh @@ -18,7 +18,7 @@ docker run --rm --network=host --name=$server_name \ --runtime=nvidia --gpus="$GPU_COUNT" --ipc=host --privileged --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 \ -v $HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE \ -v $GITHUB_WORKSPACE:/workspace/ -w /workspace/ \ --e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e SWEBENCH_GEN_MODE -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ +-e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e IS_AGENTIC -e SCENARIO_TYPE -e SWEBENCH_GEN_MODE -e SWEBENCH_USE_MODAL -e MODAL_TOKEN_ID -e MODAL_TOKEN_SECRET -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ -e PROFILE -e SGLANG_TORCH_PROFILER_DIR -e VLLM_TORCH_PROFILER_DIR -e VLLM_RPC_TIMEOUT \ -e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e TORCH_CUDA_ARCH_LIST="9.0" -e CUDA_DEVICE_ORDER=PCI_BUS_ID -e CUDA_VISIBLE_DEVICES \ --entrypoint=/bin/bash \ diff --git a/runners/launch_mi325x-tw.sh b/runners/launch_mi325x-tw.sh index 0b6e629760..7d0bc63734 100644 --- a/runners/launch_mi325x-tw.sh +++ b/runners/launch_mi325x-tw.sh @@ -26,7 +26,7 @@ docker run --rm --network=host --name="$server_name" \ --security-opt seccomp=unconfined --cap-add=SYS_PTRACE \ -v "$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE" \ -v "$GITHUB_WORKSPACE:/workspace/" -w /workspace/ \ --e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e SWEBENCH_GEN_MODE -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT="$PORT" \ +-e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e PP_SIZE -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e EVAL_FRAMEWORK -e EVAL_LIMIT -e EVAL_SUITE -e IS_AGENTIC -e SCENARIO_TYPE -e SWEBENCH_GEN_MODE -e SWEBENCH_USE_MODAL -e MODAL_TOKEN_ID -e MODAL_TOKEN_SECRET -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT="$PORT" \ -e DP_ATTENTION -e EP_SIZE -e DP_SIZE -e EVAL_MAX_MODEL_LEN -e SPEC_DECODING -e NUM_SPEC_TOKENS \ -e PROFILE -e SGLANG_TORCH_PROFILER_DIR -e VLLM_TORCH_PROFILER_DIR -e VLLM_RPC_TIMEOUT \ -e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e CUDA_DEVICE_ORDER=PCI_BUS_ID \ diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index 3d9a7c76e8..5a906978b6 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -15,14 +15,17 @@ "EVAL_LIMIT", "EVAL_SUITE", "SWEBENCH_GEN_MODE", + "SWEBENCH_USE_MODAL", + "MODAL_TOKEN_ID", + "MODAL_TOKEN_SECRET", + "IS_AGENTIC", + "SCENARIO_TYPE", "IS_MULTINODE",""" LM_EVAL_COMMAND = 'run_eval --framework lm-eval --port "$PORT" || eval_rc=$?' GENERIC_EVAL_COMMAND = 'run_eval --port "$PORT" || eval_rc=$?' EVAL_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" -VERIFIER_ARTIFACT_COPY = """cp -v results*.json /logs/eval_results/ 2>/dev/null || true -cp -v *_vendor_report.json *_vendor_results.jsonl bfcl_report.json bfcl_upstream_artifacts.tar.gz /logs/eval_results/ 2>/dev/null || true -cp -v sample*.jsonl /logs/eval_results/ 2>/dev/null || true""" +VERIFIER_ARTIFACT_COPY = 'stage_eval_artifacts /logs/eval_results "$PWD" || true' def prepare_replacements( diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 5c49591b5b..3a16511b21 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -102,12 +102,15 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert do_sweep.read_text().count('"EVAL_CONC"') == 1 assert do_sweep.read_text().count('"EVAL_LIMIT"') == 1 assert do_sweep.read_text().count('"SWEBENCH_GEN_MODE"') == 1 + assert do_sweep.read_text().count('"SWEBENCH_USE_MODAL"') == 1 + assert do_sweep.read_text().count('"MODAL_TOKEN_ID"') == 1 + assert do_sweep.read_text().count('"MODAL_TOKEN_SECRET"') == 1 + assert do_sweep.read_text().count('"IS_AGENTIC"') == 1 + assert do_sweep.read_text().count('"SCENARIO_TYPE"') == 1 assert 'run_eval --port "$PORT"' in eval_script.read_text() assert "--framework lm-eval" not in eval_script.read_text() - assert "*_vendor_report.json" in eval_script.read_text() - assert "bfcl_report.json" in eval_script.read_text() - assert "*_vendor_results.jsonl" in eval_script.read_text() - assert "bfcl_upstream_artifacts.tar.gz" in eval_script.read_text() + assert 'stage_eval_artifacts /logs/eval_results "$PWD" || true' in eval_script.read_text() + assert "cp -v" not in eval_script.read_text() assert "already patched" in second.stdout @@ -330,6 +333,58 @@ def test_gb200_dynamo_kimi_recipes_configure_tool_parser() -> None: +def test_dynamo_sglang_agentic_recipes_parse_tools_at_frontend() -> None: + recipe_roots = ( + ( + REPO_ROOT + / "benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic", + ("deepseekv4", "deepseek-v4"), + ), + ( + REPO_ROOT + / "benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic", + ("qwen3_coder", "qwen3"), + ), + ) + checked = 0 + + for recipe_root, (tool_parser, reasoning_parser) in recipe_roots: + for recipe_path in recipe_root.glob("*.yaml"): + recipe = yaml.safe_load(recipe_path.read_text()) + frontend = recipe["frontend"] + if frontend["type"] != "dynamo": + continue + args = frontend["args"] + assert args["dyn-chat-processor"] == "sglang", recipe_path + assert args["tool-call-parser"] == tool_parser, recipe_path + assert args["reasoning-parser"] == reasoning_parser, recipe_path + checked += 1 + + assert checked == 14 + + +def test_swebench_container_paths_forward_modal_credentials() -> None: + paths = ( + REPO_ROOT / "benchmarks/multi_node/llm-d/submit.sh", + REPO_ROOT / "benchmarks/multi_node/llm-d/job.slurm", + REPO_ROOT / "runners/launch_h100-cr.sh", + REPO_ROOT / "runners/launch_mi325x-tw.sh", + ) + + for path in paths: + content = path.read_text() + assert "SWEBENCH_USE_MODAL" in content, path + assert "MODAL_TOKEN_ID" in content, path + assert "MODAL_TOKEN_SECRET" in content, path + assert "IS_AGENTIC" in content, path + assert "SCENARIO_TYPE" in content, path + if path.name == "job.slurm" and "llm-d" in path.parts: + assert "-e MODAL_TOKEN_ID \\" in content + assert "-e MODAL_TOKEN_SECRET \\" in content + assert "-e MODAL_TOKEN_ID=" not in content + assert "-e MODAL_TOKEN_SECRET=" not in content + + def test_b200_kimi_recipe_uses_available_roce_devices() -> None: recipe_path = ( REPO_ROOT diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 784bd39579..df4d96f5a0 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -119,6 +119,22 @@ two-case smoke against their OpenAI-compatible frontend. Eval-only launchers restore real block verification before submitting recipes that otherwise use synthetic acceptance for throughput. +### Kimi full tool-call schema diagnostic + +`kimi_tool_call_schema_full` runs the same pinned upstream test module with +`--selection all`. It evaluates all 204 selected Walle schema cases in +non-streaming and streaming modes, for 408 reported outcomes. Eight pytest +workers share a two-hour whole-suite timeout. The native report must declare +the exact selected suite and line identities, contain both modes for every +case, and reconcile all outcome counts before projection. + +Select it explicitly with `eval-framework: kimi-vendor` and +`eval-suite: kimi_tool_call_schema_full`. Its threshold is `0.0`, so model +quality is diagnostic while setup, timeout, malformed-report, and integration +failures still fail through the standard zero-effective-sample error path. The +full suite reuses the smoke's pinned checkout, retry policy, result envelope, +artifact staging, collector, and dashboard path. + ### MiniMax provider compatibility smoke The Phase 1 MiniMax smoke is opt-in and applies to supported models exposing @@ -156,8 +172,8 @@ local runner supplies `Authorization: Bearer EMPTY`. The smoke uses `temperature: 0`, `top_p: 1`, and `max_tokens: 40960`. The token budget matches the pinned verifier's MiniMax M3 default and prevents a valid tool-call response from ending at the model's common 2048-token generation default. The request -has a 180-second timeout by default and at most one retry for transport -failures, HTTP 429, or HTTP 5xx responses (two total attempts); a hard +has a 180-second timeout by default and at most three retries for transport +failures, HTTP 429, or HTTP 5xx responses (four total attempts); a hard 900-second global bound covers the smoke. `minimax_vendor_report.json` is the native report. It preserves the raw @@ -187,6 +203,36 @@ pass-at-k behavior, streaming behavior, parallel-call behavior, multi-turn tool execution, language following, scenario key-order recall, or general agent quality. +### MiniMax M3 full provider diagnostic + +`minimax_m3_full` is an explicit, non-gating expansion of the smoke to all 102 +rows in the pinned MiniMax Provider Verifier dataset: + +```bash +source benchmarks/benchmark_lib.sh +export EVAL_FRAMEWORK=minimax-vendor +export EVAL_SUITE=minimax_m3_full +export MODEL_NAME='' +run_eval --port "$PORT" +append_lm_eval_summary +python3 utils/evals/validate_scores.py +``` + +The runner downloads only the eight source and validator files allowlisted in +`utils/evals/minimax_m3_full_eval.py` at commit +`85bf180e54e2ab0b31595cfdc697116c4760876d`, verifies each SHA256, and executes +the pinned `verify.py` once. It uses five workers, a 600-second request timeout, +three upstream retries, and a seven-hour whole-suite timeout. The workflow +retains at least one hour for artifact staging, score validation, and cleanup. + +The native files are `minimax_vendor_report.json` and +`minimax_vendor_results.jsonl`. The compatibility result publishes task +`minimax_m3_full` with the native `tool_calls_match_rate`, requires exactly 102 +successful result rows, and rejects transport failures or inconsistent +summaries. Its threshold is `0.0`, so this suite is diagnostic during the first +rollout. Setup, transport, timeout, malformed-output, and integration failures +still fail through the standard zero-effective-sample error path. + ### BFCL V4 deterministic tool-use smoke The BFCL smoke is opt-in for models served through an OpenAI-compatible @@ -202,9 +248,12 @@ export EVAL_SUITE=bfcl_smoke export EVAL_RESULT_DIR="$(mktemp -d /tmp/eval_out-XXXXXX)" run_eval --port "$PORT" append_lm_eval_summary -python3 utils/evals/validate_scores.py --metric-prefix 'acc,' +python3 utils/evals/validate_scores.py ``` +The validator reads BFCL's declared `acc` metric from the compatibility result, +so workflows do not need a framework-specific metric override. + The runtime pins [`bfcl-eval==2026.3.23`](https://pypi.org/project/bfcl-eval/2026.3.23/), built from Gorilla commit @@ -339,7 +388,7 @@ All benchmark scripts in `benchmarks/` follow one of two flows: # 4. Run evals: if [ "${RUN_EVAL}" = "true" ]; then run_eval --framework lm-eval --port "$PORT" - append_lm_eval_summary # Writes meta_env.json and moves artifacts + append_lm_eval_summary # Writes meta_env.json and stages artifacts fi # Eval-only mode (EVAL_ONLY=true): @@ -357,9 +406,9 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `run_eval` | Unified entrypoint - dispatches to framework-specific runner | | `run_lm_eval` | Runs lm-eval harness against the OpenAI-compatible endpoint | | `run_kimi_vendor_eval` | Selects and runs a pinned Kimi Vendor Verifier suite | -| `run_minimax_vendor_eval` | Runs the pinned single-case MiniMax provider smoke | -| `run_bfcl_eval` | Runs the pinned four-case BFCL V4 smoke | -| `append_lm_eval_summary` | Writes `meta_env.json` and moves eval artifacts to workspace | +| `run_minimax_vendor_eval` | Selects the pinned MiniMax smoke or full diagnostic | +| `run_bfcl_eval` | Selects a pinned BFCL V4 smoke or model-quality suite | +| `append_lm_eval_summary` | Writes `meta_env.json` and stages eval artifacts in the workspace | | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_prepare_vendor_verifier_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime for provider verifiers | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | @@ -371,6 +420,11 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | +`EVAL_FRAMEWORK` is the orchestration-level selection and takes precedence over +legacy `--framework lm-eval` arguments embedded in fixed-sequence recipes. +Without that environment variable, an explicit `--framework` argument takes +precedence over the scenario default. + ### Single-node For default lm-eval jobs in eval-only mode (`EVAL_ONLY=true`), the benchmark script computes `EVAL_MAX_MODEL_LEN` via `compute_eval_context_length`, starts the server with that context length, skips throughput, and runs lm-eval. Each framework wires that context differently (`--context-length` for SGLang, `--max_seq_len` for TRT-LLM). diff --git a/utils/evals/bfcl_eval.py b/utils/evals/bfcl_adapter.py old mode 100755 new mode 100644 similarity index 95% rename from utils/evals/bfcl_eval.py rename to utils/evals/bfcl_adapter.py index d649edf7f2..72af4cb588 --- a/utils/evals/bfcl_eval.py +++ b/utils/evals/bfcl_adapter.py @@ -200,8 +200,15 @@ def _nonempty_string(value: str) -> str: def _absolute_http_url(value: str) -> str: normalized = _nonempty_string(value).rstrip("/") parsed = urllib.parse.urlsplit(normalized) - if parsed.scheme not in {"http", "https"} or not parsed.netloc: - raise argparse.ArgumentTypeError("must be an absolute HTTP(S) URL") + if ( + parsed.scheme not in {"http", "https"} + or not parsed.netloc + or parsed.query + or parsed.fragment + ): + raise argparse.ArgumentTypeError( + "must be an absolute HTTP(S) URL without query or fragment" + ) if parsed.path.rstrip("/").endswith("/chat/completions"): raise argparse.ArgumentTypeError( "must be an API root URL; the OpenAI client appends /chat/completions" @@ -450,6 +457,12 @@ def _prepare_output_paths(output_dir: Path) -> tuple[Path, Path]: compatibility_path = output_dir / COMPATIBILITY_FILENAME return native_path, compatibility_path +def _clear_upstream_modules() -> None: + """Reload BFCL's import-time paths and limits for each adapter invocation.""" + for module_name in tuple(sys.modules): + if module_name == "bfcl_eval" or module_name.startswith("bfcl_eval."): + sys.modules.pop(module_name, None) + def _write_id_map( project_root: Path, case_ids_by_category: Mapping[str, tuple[str, ...]] @@ -483,26 +496,17 @@ def _load_dataset_helpers( maximum_step_limit: int | None, ) -> tuple[Callable[[str], Any], Callable[[list[str]], Any], Callable[[Any], Any]]: """Import BFCL's pinned dataset helpers only when a full suite is selected.""" - adapter_directory = Path(__file__).resolve().parent - original_sys_path = sys.path[:] - sys.path[:] = [ - entry - for entry in sys.path - if Path(entry or os.curdir).resolve() != adapter_directory - ] - try: - if maximum_step_limit is not None: - import bfcl_eval.constants.default_prompts as bfcl_prompts - - # This must happen before importing utils/base_handler for multi-turn. - bfcl_prompts.MAXIMUM_STEP_LIMIT = maximum_step_limit - from bfcl_eval.utils import ( - load_dataset_entry, - parse_test_category_argument, - sort_key, - ) - finally: - sys.path[:] = original_sys_path + if maximum_step_limit is not None: + import bfcl_eval.constants.default_prompts as bfcl_prompts + + # This must happen before importing utils/base_handler for multi-turn. + bfcl_prompts.MAXIMUM_STEP_LIMIT = maximum_step_limit + from bfcl_eval.utils import ( + load_dataset_entry, + parse_test_category_argument, + sort_key, + ) + return load_dataset_entry, parse_test_category_argument, sort_key @@ -609,28 +613,16 @@ def _run_upstream( os.environ["OPENAI_BASE_URL"] = base_url os.environ["OPENAI_API_KEY"] = api_key - # The adapter filename intentionally matches the installed package. When the - # file is executed directly, hide its directory during package resolution. - adapter_directory = Path(__file__).resolve().parent - original_sys_path = sys.path[:] - sys.path[:] = [ - entry - for entry in sys.path - if Path(entry or os.curdir).resolve() != adapter_directory - ] - try: - if suite.maximum_step_limit is not None: - import bfcl_eval.constants.default_prompts as bfcl_prompts - - bfcl_prompts.MAXIMUM_STEP_LIMIT = suite.maximum_step_limit - import bfcl_eval.constants.model_config as bfcl_model_config - from bfcl_eval.__main__ import evaluate, generate - from bfcl_eval.constants.model_config import ModelConfig - from bfcl_eval.model_handler.api_inference.openai_completion import ( - OpenAICompletionsHandler, - ) - finally: - sys.path[:] = original_sys_path + if suite.maximum_step_limit is not None: + import bfcl_eval.constants.default_prompts as bfcl_prompts + + bfcl_prompts.MAXIMUM_STEP_LIMIT = suite.maximum_step_limit + import bfcl_eval.constants.model_config as bfcl_model_config + from bfcl_eval.__main__ import evaluate, generate + from bfcl_eval.constants.model_config import ModelConfig + from bfcl_eval.model_handler.api_inference.openai_completion import ( + OpenAICompletionsHandler, + ) request_failures: SimpleQueue[Exception] = SimpleQueue() class BoundedOpenAICompletionsHandler(OpenAICompletionsHandler): @@ -939,6 +931,7 @@ def run_evaluation( raise TypeError("upstream_runner must be callable") os.environ["BFCL_PROJECT_ROOT"] = str(bfcl_project_root) + _clear_upstream_modules() _write_upstream_attribution(bfcl_project_root) selected_case_ids = _build_suite_case_ids(suite) _write_id_map(bfcl_project_root, selected_case_ids) diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 3845a61e05..9007393f98 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -327,15 +327,13 @@ def run_evaluation( report, task_name=task_name, ) - if task_name == FULL_TASK_NAME: - completed_successfully = subprocess_rc == 0 or ( - subprocess_rc == 1 and not all_passed - ) - invalid_exit = not completed_successfully - else: - completed_successfully = subprocess_rc == 0 and all_passed - invalid_exit = subprocess_rc != 0 and all_passed - if invalid_exit: + valid_outcome = (subprocess_rc == 0 and all_passed) or ( + subprocess_rc == 1 and not all_passed + ) + completed_successfully = subprocess_rc == 0 and all_passed + if task_name == FULL_TASK_NAME and valid_outcome: + completed_successfully = True + if not valid_outcome: integration_error = RuntimeError( f"upstream verifier exited with code {subprocess_rc}" ) @@ -343,7 +341,7 @@ def run_evaluation( model, 0.0, task_name=task_name, - n_samples=expected_total, + n_samples=0, integration_error=integration_error, ) completed_successfully = False diff --git a/utils/evals/minimax_m3_full_eval.py b/utils/evals/minimax_m3_full_eval.py index af8d1fa2cd..0fdf9f6c86 100755 --- a/utils/evals/minimax_m3_full_eval.py +++ b/utils/evals/minimax_m3_full_eval.py @@ -10,6 +10,7 @@ import os import subprocess import urllib.error +import urllib.parse import urllib.request from collections.abc import Callable, Mapping, Sequence from datetime import datetime, timezone @@ -21,7 +22,7 @@ ADAPTER_NAME = "minimax-provider-verifier" NATIVE_REPORT_FILENAME = "minimax_vendor_report.json" NATIVE_RESULTS_FILENAME = "minimax_vendor_results.jsonl" -COMPATIBILITY_GLOB = "results_minimax_vendor_full_*.json" +COMPATIBILITY_GLOB = "results_minimax_vendor_*.json" EXPECTED_RESULT_COUNT = 102 UPSTREAM_REF = "85bf180e54e2ab0b31595cfdc697116c4760876d" UPSTREAM_BASE_URL = ( @@ -43,7 +44,7 @@ } MAX_SOURCE_BYTES = 16 * 1024 * 1024 DOWNLOAD_TIMEOUT_SECONDS = 60 -UPSTREAM_TIMEOUT_SECONDS = 12 * 60 * 60 +UPSTREAM_TIMEOUT_SECONDS = 7 * 60 * 60 Runner = Callable[..., subprocess.CompletedProcess[Any]] Fetcher = Callable[[str], bytes] @@ -180,10 +181,19 @@ def build_verifier_command( output_dir: Path, ) -> list[str]: """Build the single pinned-upstream invocation for all 102 rows.""" - if not model.strip(): + if not isinstance(model, str) or not model.strip(): raise ValueError("model must be a non-empty string") - if not base_url.startswith(("http://", "https://")): - raise ValueError("base_url must be an absolute HTTP(S) URL") + normalized_base_url = base_url.strip().rstrip("/") + parsed_base_url = urllib.parse.urlsplit(normalized_base_url) + if ( + parsed_base_url.scheme not in {"http", "https"} + or not parsed_base_url.netloc + or parsed_base_url.query + or parsed_base_url.fragment + ): + raise ValueError( + "base_url must be an absolute HTTP(S) URL without query or fragment" + ) extra_body = json.dumps( {"temperature": 0, "top_p": 1, "max_tokens": 40960}, separators=(",", ":"), @@ -195,7 +205,7 @@ def build_verifier_command( "--model", model, "--base-url", - base_url.rstrip("/"), + normalized_base_url, "--api-key", "EMPTY", "--concurrency", @@ -363,29 +373,28 @@ def project_native_artifacts(*, output_dir: Path, model: str) -> Path: def publish_failure(*, output_dir: Path, model: str, error: BaseException) -> Path: - """Publish compatibility and native diagnostics without hiding partial output.""" + """Publish canonical failure metadata while retaining partial result rows.""" output_dir.mkdir(parents=True, exist_ok=True) native_report_path = output_dir / NATIVE_REPORT_FILENAME native_results_path = output_dir / NATIVE_RESULTS_FILENAME - if not native_report_path.exists(): - _write_json( - native_report_path, - { - "verifier": ADAPTER_NAME, - "task": TASK_NAME, - "model": model, - "completed": False, - "threshold": 0.0, - "source": { - "ref": UPSTREAM_REF, - "sample_sha256": EXPECTED_SAMPLE_SHA256, - }, - "success_count": 0, - "failure_count": EXPECTED_RESULT_COUNT, - "tool_calls_match_rate": 0.0, - "integration_error": _error_dict(error), + _write_json( + native_report_path, + { + "verifier": ADAPTER_NAME, + "task": TASK_NAME, + "model": model, + "completed": False, + "threshold": 0.0, + "source": { + "ref": UPSTREAM_REF, + "sample_sha256": EXPECTED_SAMPLE_SHA256, }, - ) + "success_count": 0, + "failure_count": EXPECTED_RESULT_COUNT, + "tool_calls_match_rate": 0.0, + "integration_error": _error_dict(error), + }, + ) if not native_results_path.exists(): native_results_path.write_text( json.dumps( @@ -497,6 +506,7 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 return 0 if args.command == "failure": + (args.output_dir / NATIVE_RESULTS_FILENAME).unlink(missing_ok=True) publish_failure( output_dir=args.output_dir, model=args.model, diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index c76461a59f..463f786a30 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse -import copy import hashlib import http.client import json @@ -139,9 +138,6 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: raise ValueError("fixture must contain exactly one row") rows: list[dict[str, Any]] = [] - expected_checks = { - 71: [], - } for position, raw_row in enumerate(raw_rows): row = dict(_mapping(raw_row, f"fixture.rows[{position}]")) data_index = row.get("data_index") @@ -157,20 +153,12 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: if case_digest != EXPECTED_CASE_SHA256[data_index]: raise ValueError(f"fixture row {data_index} differs from pinned upstream") _validate_messages(row.get("messages"), f"fixture.rows[{position}].messages") - check_types = row.get("check_type", []) - if check_types != expected_checks[data_index]: + if row.get("check_type", []) != []: raise ValueError(f"fixture row {data_index} has unexpected check_type") - if data_index == 0: - if "expected_tool_call" in row or "tools" in row: - raise ValueError("fixture row 0 must remain the language-only case") - else: - _validate_tools(row.get("tools"), f"fixture.rows[{position}].tools") - expected_label = data_index == 71 - if row.get("expected_tool_call") is not expected_label: - raise ValueError( - f"fixture row {data_index} has an invalid expected label" - ) - rows.append(copy.deepcopy(row)) + _validate_tools(row.get("tools"), f"fixture.rows[{position}].tools") + if row.get("expected_tool_call") is not True: + raise ValueError(f"fixture row {data_index} has an invalid expected label") + rows.append(row) return dict(root), rows @@ -180,8 +168,15 @@ def build_endpoint(base_url: str) -> str: raise ValueError("base_url must be a non-empty string") normalized = base_url.strip().rstrip("/") parsed = urllib.parse.urlsplit(normalized) - if parsed.scheme not in {"http", "https"} or not parsed.netloc: - raise ValueError("base_url must be an absolute HTTP(S) URL") + if ( + parsed.scheme not in {"http", "https"} + or not parsed.netloc + or parsed.query + or parsed.fragment + ): + raise ValueError( + "base_url must be an absolute HTTP(S) URL without query or fragment" + ) return f"{normalized}/chat/completions" @@ -189,7 +184,7 @@ def prepare_request(row: Mapping[str, Any], model: str) -> dict[str, Any]: """Strip evaluator fields and apply the fixed smoke sampling overrides.""" if not isinstance(model, str) or not model.strip(): raise ValueError("model must be a non-empty string") - request = copy.deepcopy(dict(row)) + request = dict(row) for field in ("data_index", "check_type", "expected_tool_call", "scenario_check"): request.pop(field, None) request.update( @@ -294,75 +289,17 @@ def _validate_chat_completion_response(value: Any) -> Mapping[str, Any]: return response -# Adapted verbatim from pinned validator/tool_calls.py. -_COMMON_COMMANDS = [ - "ls ", - "cat ", - "git ", - "npm ", - "npx ", - "cd ", - "cp ", - "mv ", - "rm ", - "mkdir ", - "chmod ", - "chown ", - "find ", - "grep ", - "curl ", - "wget ", - "pip ", -] - - -def _is_shell_c_invocation(cmd: list[Any]) -> bool: - if not cmd or len(cmd) < 3: - return False - shell = cmd[0] - if shell not in ( - "bash", - "sh", - "zsh", - "/bin/bash", - "/bin/sh", - "/bin/zsh", - "/usr/bin/bash", - "/usr/bin/sh", - "/usr/bin/zsh", - ): - return False - for arg in cmd[1:]: - if arg in ("-c", "-lc"): - return True - if arg in ("-l", "--login"): - continue - break - return False - - -def is_valid_array_command(cmd: Any) -> bool: - if not isinstance(cmd, list) or len(cmd) == 0: - return False - if _is_shell_c_invocation(cmd): - return True - for elem in cmd: - if not isinstance(elem, str): - return False - if " " in elem: - for prefix in _COMMON_COMMANDS: - if elem.startswith(prefix): - return False - return not (len(cmd) == 1 and " " in cmd[0]) +def _require_tool_validation() -> None: + """Fail setup before requests if the pinned schema dependency is unavailable.""" + try: + import jsonschema # noqa: F401 + except ImportError as exc: + raise RuntimeError("jsonschema is required for tool-call validation") from exc def validate_tool_call(tool_call: Any, tools: list[dict[str, Any]]) -> bool: - """Apply pinned JSON Schema and array-command validation lazily.""" - try: - # Lazy by design: --integration-error must work if dependency setup failed. - from jsonschema import ValidationError, validate - except ImportError: - return False + """Apply pinned JSON Schema validation.""" + from jsonschema import ValidationError, validate try: call = _mapping(tool_call, "tool_call") @@ -382,15 +319,6 @@ def validate_tool_call(tool_call: Any, tools: list[dict[str, Any]]) -> bool: if isinstance(args, str): args = json.loads(args) validate(instance=args, schema=schema) - for param_name, param_schema in schema.get("properties", {}).items(): - if ( - param_name == "command" - and param_schema.get("type") == "array" - and param_schema.get("items", {}).get("type") == "string" - ): - cmd_value = args.get(param_name) - if cmd_value is not None and not is_valid_array_command(cmd_value): - return False return True except (json.JSONDecodeError, ValidationError): return False @@ -424,92 +352,6 @@ def validate_tool_calls( return result -# Adapted verbatim from pinned validator/russian_characters.py. -def not_contains_russian_characters_unicode(text: str) -> bool: - for char in text: - char_code = ord(char) - if 0x0400 <= char_code <= 0x04FF: - return False - return True - - -def validate_language(status: str, resp_content: Any) -> dict[str, Any]: - result: dict[str, Any] = { - "language_following_checked": False, - "language_following_valid": None, - } - if status != "success" or not resp_content: - return result - result["language_following_checked"] = True - result["language_following_valid"] = not_contains_russian_characters_unicode( - resp_content - ) - return result - - -# Adapted verbatim from pinned validator/scenario_check.py. -def _extract_expected_order(request: dict[str, Any]) -> list[str] | None: - tools = request.get("tools") - if not tools or not isinstance(tools, list): - return None - params = tools[0].get("function", {}).get("parameters", {}) - if not params: - return None - if "properties" in params: - return list(params["properties"].keys()) - schema_keywords = { - "type", - "description", - "required", - "additionalProperties", - "$schema", - "items", - "enum", - "default", - } - keys = [key for key in params if key not in schema_keywords] - return keys if keys else None - - -def _get_visible_content(text: str) -> str: - return re.sub(r".*?", "", text, flags=re.DOTALL).strip() - - -def _extract_actual_order(text: str, expected: list[str]) -> list[str]: - positions = [] - for param in expected: - index = text.find(param) - if index != -1: - positions.append((index, param)) - positions.sort(key=lambda item: item[0]) - return [param for _, param in positions] - - -def validate_scenario( - request: dict[str, Any], status: str, resp_content: Any -) -> dict[str, Any]: - result: dict[str, Any] = { - "scenario_check_checked": False, - "scenario_check_valid": None, - "scenario_check_detail": None, - } - if status != "success" or not resp_content: - return result - expected_order = _extract_expected_order(request) - if not expected_order: - return result - visible = _get_visible_content(resp_content) - actual_order = _extract_actual_order(visible, expected_order) - result["scenario_check_checked"] = True - result["scenario_check_valid"] = ( - len(actual_order) >= 2 and actual_order == expected_order[: len(actual_order)] - ) - result["scenario_check_detail"] = { - "expected": expected_order, - "actual": actual_order, - } - return result - # Adapted verbatim from pinned verify.py::_is_error_only_reasoning_response. def _is_error_only_reasoning_response(response: Any) -> bool: @@ -589,9 +431,7 @@ def _evaluate_case( response = None suite_timed_out = True break - response = copy.deepcopy( - dict(_validate_chat_completion_response(raw_response)) - ) + response = dict(_validate_chat_completion_response(raw_response)) status = "success" request_error = None break @@ -614,7 +454,7 @@ def _evaluate_case( suite_timed_out = True break - finish_reason, resp_content = _choice_fields(response) + finish_reason, _ = _choice_fields(response) result: dict[str, Any] = { "data_index": row["data_index"], "status": status, @@ -627,17 +467,13 @@ def _evaluate_case( else {"error": _error_dict(request_error or RuntimeError("request failed"))}, "error_only_reasoning_checked": 1, "error_only_reasoning": _is_error_only_reasoning_response(response), + "integration_failure": isinstance( + request_error, (TransportError, TimeoutError, OSError) + ), } - check_types = row.get("check_type", []) try: - if check_types: - if "contains_russian_characters_unicode" in check_types: - result.update(validate_language(status, resp_content)) - if "scenario_check" in check_types: - result.update(validate_scenario(prepared, status, resp_content)) - else: - result.update(validate_tool_calls(prepared, response, status)) + result.update(validate_tool_calls(prepared, response, status)) except Exception as exc: # noqa: BLE001 - validators must not abort the report result["validator_error"] = _error_dict(exc) @@ -658,16 +494,6 @@ def _evaluate_case( and result.get("tool_calls_valid") is not True ): failures.append("tool_call_schema") - if ( - "contains_russian_characters_unicode" in check_types - and result.get("language_following_valid") is not True - ): - failures.append("language_following") - if ( - "scenario_check" in check_types - and result.get("scenario_check_valid") is not True - ): - failures.append("scenario_check") if "validator_error" in result: failures.append("validator_error") @@ -899,6 +725,7 @@ def _failed_case_result(row: Mapping[str, Any], exc: BaseException) -> dict[str, "case_passed": False, "failures": ["adapter_error"], "suite_timed_out": isinstance(exc, SuiteTimeoutError), + "integration_failure": True, } @@ -960,7 +787,14 @@ def run_evaluation( raise ValueError("api_key must be a non-empty string") endpoint = build_endpoint(base_url) fixture_metadata, rows = load_fixture(fixture_path) - except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + _require_tool_validation() + except ( + OSError, + RuntimeError, + ValueError, + TypeError, + json.JSONDecodeError, + ) as exc: _write_json( native_path, _native_report( @@ -997,10 +831,25 @@ def run_evaluation( results.append(result) timed_out = any(result["suite_timed_out"] for result in results) + failed_integration = next( + (result for result in results if result["integration_failure"]), + None, + ) integration_error: BaseException | None = None - completed = not timed_out if timed_out: integration_error = SuiteTimeoutError("global suite timeout exceeded") + elif failed_integration is not None: + error = failed_integration["response"]["error"] + message = str(error.get("message", "request failed")) + if error.get("type") == "TransportError": + integration_error = TransportError(message) + elif error.get("type") in {"TimeoutError", "SuiteTimeoutError"}: + integration_error = TimeoutError(message) + else: + integration_error = RuntimeError( + f"{error.get('type', 'adapter error')}: {message}" + ) + completed = integration_error is None native = _native_report( model=model, endpoint=endpoint, @@ -1010,7 +859,7 @@ def run_evaluation( integration_error=integration_error, ) passed_count = native["summary"]["passed_count"] - effective = sum(not result["suite_timed_out"] for result in results) + effective = len(results) if completed else 0 score = passed_count / len(EXPECTED_INDICES) if completed else 0.0 compatibility = _compatibility_result( model, diff --git a/utils/evals/test_batched_eval.py b/utils/evals/test_batched_eval.py index 1c8378ca7a..a4aace9389 100644 --- a/utils/evals/test_batched_eval.py +++ b/utils/evals/test_batched_eval.py @@ -348,8 +348,4 @@ def test_amd_multinode_container_forwards_eval_concurrency_list() -> None: ).read_text() assert 'expected_concs="${EVAL_CONC}"' in workflow assert 'validate_scores.py --expected-concs "${expected_concs}"' in workflow - assert 'if [[ "${EVAL_FRAMEWORK}" == "bfcl" ]]; then' in workflow - assert ( - "validate_scores.py --metric-prefix 'acc,' " - '--expected-concs "${expected_concs}"' - ) in workflow + assert "--metric-prefix" not in workflow diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index 4a3cb00dac..41092812a2 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -10,7 +10,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) -import bfcl_eval as be +import bfcl_adapter as be import validate_scores as vs @@ -70,6 +70,39 @@ def import_without_yaml(name, *args, **kwargs): assert thresholds["default"]["bfcl_smoke"] == 0.75 assert thresholds["default"]["bfcl_parallel"] == 0.0 +def test_score_validator_uses_declared_bfcl_metric( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + result = { + "results": {"bfcl_smoke": {"acc,none": 0.8, "acc_stderr,none": 0.1}}, + "configs": { + "bfcl_smoke": { + "metric_list": [{"metric": "acc", "aggregation": "mean"}] + } + }, + "n-samples": {"bfcl_smoke": {"original": 4, "effective": 4}}, + } + (tmp_path / "results_bfcl.json").write_text(json.dumps(result)) + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, "argv", ["validate_scores.py"]) + + assert vs.main() == 0 + + +def test_adapter_module_does_not_collide_with_upstream_package( + monkeypatch: pytest.MonkeyPatch, +) -> None: + upstream = object() + child = object() + monkeypatch.setitem(sys.modules, "bfcl_eval", upstream) + monkeypatch.setitem(sys.modules, "bfcl_eval.constants", child) + + be._clear_upstream_modules() + + assert be.__name__ == "bfcl_adapter" + assert "bfcl_eval" not in sys.modules + assert "bfcl_eval.constants" not in sys.modules + def _write_result( project_root: Path, @@ -285,14 +318,20 @@ def test_cli_rejects_invalid_positive_values( ) -def test_cli_rejects_chat_completions_endpoint_instead_of_api_root( - tmp_path: Path, -) -> None: +@pytest.mark.parametrize( + "base_url", + ( + "http://localhost/v1/chat/completions", + "http://localhost/v1?mode=test", + "http://localhost/v1#fragment", + ), +) +def test_cli_rejects_invalid_api_root(tmp_path: Path, base_url: str) -> None: with pytest.raises(SystemExit): be.parse_args( [ "--base-url", - "http://localhost/v1/chat/completions", + base_url, "--model", "model-a", "--output-dir", diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index 43b3c82393..6cf352f9d7 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -189,11 +189,20 @@ def test_builds_dsv4_thinking_command(tmp_path: Path) -> None: @pytest.mark.parametrize( - ("stream_status", "return_code", "expected_pass", "expected_score"), ( - ("passed", 0, True, 1.0), - ("passed", 1, False, 0.0), - ("failed", 1, False, 0.5), + "stream_status", + "return_code", + "expected_pass", + "expected_score", + "expected_n_eff", + "expected_error", + ), + ( + ("passed", 0, True, 1.0, 2, None), + ("passed", 1, False, 0.0, 0, "RuntimeError"), + ("failed", 0, False, 0.0, 0, "RuntimeError"), + ("failed", 1, False, 0.5, 2, None), + ("failed", 2, False, 0.0, 0, "RuntimeError"), ), ) def test_projects_upstream_outcomes( @@ -203,6 +212,8 @@ def test_projects_upstream_outcomes( return_code: int, expected_pass: bool, expected_score: float, + expected_n_eff: int, + expected_error: str | None, ) -> None: output_dir = tmp_path / "output" native_bytes = json.dumps(_report(stream_status)).encode() @@ -231,10 +242,14 @@ def fake_run( assert invocation["check"] is False assert invocation["timeout"] == kve.DEFAULT_TIMEOUT_SECONDS assert _score(output_dir) == expected_score - assert _n_eff(output_dir) == 2 + assert _n_eff(output_dir) == expected_n_eff projected = _result(output_dir) assert projected["result_format"] == kve.RESULT_FORMAT assert projected["eval_adapter"] == kve.ADAPTER_NAME + if expected_error is None: + assert "integration_error" not in projected + else: + assert projected["integration_error"]["type"] == expected_error assert "lm_eval_version" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes diff --git a/utils/evals/test_minimax_m3_full_eval.py b/utils/evals/test_minimax_m3_full_eval.py index 93d8b7ba10..369062e217 100644 --- a/utils/evals/test_minimax_m3_full_eval.py +++ b/utils/evals/test_minimax_m3_full_eval.py @@ -146,6 +146,54 @@ def test_projects_exactly_102_results_from_native_match_rate_without_rewriting_r .splitlines() ) == 102 +def test_full_projection_removes_stale_smoke_result(tmp_path: Path) -> None: + stale_smoke = tmp_path / "results_minimax_vendor_2026-01-01.json" + stale_smoke.write_text("{}") + + compatibility_path = full._compatibility_path(tmp_path) + + assert not stale_smoke.exists() + assert compatibility_path.name.startswith("results_minimax_vendor_full_") + + +def test_failure_command_replaces_stale_native_artifacts(tmp_path: Path) -> None: + native_report = tmp_path / full.NATIVE_REPORT_FILENAME + native_results = tmp_path / full.NATIVE_RESULTS_FILENAME + native_report.write_text('{"stale": true}\n') + native_results.write_text('{"stale": true}\n') + + assert ( + full.main( + [ + "failure", + "--model", + "MiniMax-M3", + "--output-dir", + str(tmp_path), + "--message", + "runtime setup failed", + ] + ) + == 0 + ) + + report = json.loads(native_report.read_text(encoding="utf-8")) + assert report["completed"] is False + assert report["integration_error"]["message"] == "runtime setup failed" + result_rows = native_results.read_text(encoding="utf-8").splitlines() + assert len(result_rows) == 1 + assert json.loads(result_rows[0])["status"] == "integration_error" + + +def test_full_suite_timeout_leaves_workflow_cleanup_margin() -> None: + shortest_workflow_timeout_seconds = 480 * 60 + cleanup_margin_seconds = 60 * 60 + + assert ( + full.UPSTREAM_TIMEOUT_SECONDS + <= shortest_workflow_timeout_seconds - cleanup_margin_seconds + ) + @pytest.mark.parametrize( ("result_count", "failed_index", "message"), diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index bfe1f5d552..47489b2be9 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -250,40 +250,6 @@ def post(payload: dict[str, Any]) -> dict[str, Any]: assert native["summary"]["stop_finish_stop"] == 0 -def test_language_validator_uses_pinned_cyrillic_range() -> None: - result = mpe.validate_language("success", "Это ответ") - assert result["language_following_checked"] is True - assert result["language_following_valid"] is False - - -def test_scenario_validator_uses_visible_first_occurrence_order() -> None: - request = { - "tools": [ - { - "function": { - "parameters": { - "properties": { - "123": {}, - "some-parameter": {}, - "xyz": {}, - "another-parameter": {}, - } - } - } - } - ] - } - result = mpe.validate_scenario( - request, - "success", - "123 some-parameter xyz then some-parameter", - ) - assert result["scenario_check_checked"] is True - assert result["scenario_check_detail"] == { - "expected": ["123", "some-parameter", "xyz", "another-parameter"], - "actual": ["xyz", "some-parameter"], - } - assert result["scenario_check_valid"] is False def test_transport_retries_with_backoff_then_preserves_success( @@ -515,6 +481,14 @@ def http_post(**request: Any) -> dict[str, Any]: "tool_call_trigger", ] assert native["metrics"]["Query-Success-Rate"] == 0.0 + assert native["completed"] is False + assert native["integration_error"] == { + "type": "TransportError", + "message": "offline", + } + compatibility = _compatibility(output_dir) + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 0 + assert compatibility["integration_error"] == native["integration_error"] def test_global_deadline_caps_attempts_and_publishes_timeout_report( @@ -716,6 +690,34 @@ def http_post(**request: Any) -> Any: assert _score(output_dir) == 0.0 assert _compatibility(output_dir)["n-samples"][mpe.TASK_NAME]["effective"] == 0 +def test_missing_schema_dependency_is_an_integration_error( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + called = False + + def http_post(**request: Any) -> Any: + nonlocal called + called = True + return _response_for(request["payload"]) + + def missing_dependency() -> None: + raise RuntimeError("jsonschema is required for tool-call validation") + + monkeypatch.setattr(mpe, "_require_tool_validation", missing_dependency) + + assert not mpe.run_evaluation( + base_url="https://provider.example/v1", + api_key="secret", + model="MiniMax-M3", + output_dir=tmp_path, + http_post=http_post, + ) + assert called is False + native = _native(tmp_path) + assert native["completed"] is False + assert native["integration_error"]["type"] == "RuntimeError" + assert _compatibility(tmp_path)["n-samples"][mpe.TASK_NAME]["effective"] == 0 + @pytest.mark.parametrize( "field", diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 919f903c30..4997af0b76 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1,6 +1,7 @@ from __future__ import annotations +import hashlib import io import json import os @@ -85,6 +86,15 @@ def test_agentic_eval_only_stages_summary(): def test_fixed_seqlen_eval_only_leaves_staging_to_recipe(): assert "STAGED=summary" not in _dispatch(is_agentic="0", eval_only="true") +def test_fixed_seqlen_provider_leaves_staging_to_recipe() -> None: + output = _dispatch( + is_agentic="0", + eval_only="true", + env_fw="minimax-vendor", + ) + assert "DISPATCH=minimax-vendor" in output + assert "STAGED=summary" not in output + def test_explicit_framework_arg_overrides_scenario(): @@ -94,6 +104,13 @@ def test_explicit_framework_arg_overrides_scenario(): def test_env_framework_overrides_scenario(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="1", env_fw="lm-eval") +def test_environment_framework_overrides_legacy_recipe_argument() -> None: + assert "DISPATCH=kimi-vendor" in _dispatch( + is_agentic="1", + cli_fw="bfcl", + env_fw="kimi-vendor", + ) + def test_env_can_force_swebench_on_fixed_seqlen(): assert "DISPATCH=swebench" in _dispatch(is_agentic="0", env_fw="swebench") @@ -493,9 +510,22 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( source "$BENCHMARK_LIB" unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE unset VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR -_prepare_vendor_verifier_python() { return 12; } -_prepare_minimax_vendor_runtime() { echo "UNEXPECTED_DEPENDENCY_INSTALL"; return 99; } -python3() { printf 'ADAPTER_ARG=<%s>\n' "$@"; } +_prepare_vendor_verifier_python() { + if compgen -G "$RESULTS_DIR/results_minimax_vendor_*.json" >/dev/null \ + || [ -e "$RESULTS_DIR/minimax_vendor_report.json" ]; then + echo "STALE_MINIMAX_ARTIFACT" + return 99 + fi + mkdir -p "$PYTHON_DIR" + cat >"$PYTHON_DIR/python3" <<'PY' +#!/bin/bash +printf 'ADAPTER_ARG=<%s>\n' "$@" +PY + chmod +x "$PYTHON_DIR/python3" + export VENDOR_VERIFIER_PYTHON="$PYTHON_DIR/python3" + export VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" +} +_prepare_minimax_vendor_runtime() { return 12; } append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR" printf 'STAGED_CONC=<%s>\n' "$CONC" @@ -510,12 +540,16 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( printf 'EVAL_RC=%s\n' "$eval_rc" ''' results_dir = tmp_path / "results" + results_dir.mkdir() + (results_dir / "results_minimax_vendor_stale.json").write_text("{}") + (results_dir / "minimax_vendor_report.json").write_text("{}") result = subprocess.run( ["bash", "-c", script], env={ **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "RESULTS_DIR": str(results_dir), + "PYTHON_DIR": str(tmp_path / "python"), }, text=True, capture_output=True, @@ -524,13 +558,14 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( output = result.stdout + result.stderr assert "EVAL_RC=12" in output - assert "UNEXPECTED_DEPENDENCY_INSTALL" not in output + assert "STALE_MINIMAX_ARTIFACT" not in output + assert not (tmp_path / "python").exists() assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/minimax_provider_eval.py'}>" in output assert "ADAPTER_ARG=" in output assert f"ADAPTER_ARG=<{results_dir}>" in output assert "ADAPTER_ARG=<--integration-error>" in output assert ( - "ADAPTER_ARG=" ) in output assert f"STAGED=<{results_dir}>" in output @@ -682,12 +717,25 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, ) -> None: results_dir = tmp_path / "results" + results_dir.mkdir() + (results_dir / "results_kimi_vendor_stale.json").write_text("{}") + (results_dir / "kimi_vendor_report.json").write_text("{}") python_dir = tmp_path / "python" script = r''' source "$BENCHMARK_LIB" _prepare_vendor_verifier_python() { + if compgen -G "$RESULTS_DIR/results_kimi_vendor_*.json" >/dev/null \ + || [ -e "$RESULTS_DIR/kimi_vendor_report.json" ]; then + echo "STALE_KIMI_ARTIFACT" + return 99 + fi mkdir "$PYTHON_DIR" - VENDOR_VERIFIER_PYTHON=/unusable/bootstrap/python + cat >"$PYTHON_DIR/python3" <<'PY' +#!/bin/bash +exec /usr/bin/env python3 "$@" +PY + chmod +x "$PYTHON_DIR/python3" + VENDOR_VERIFIER_PYTHON="$PYTHON_DIR/python3" VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR } @@ -722,6 +770,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( score_files = list(results_dir.glob("results*.json")) assert "SETUP_RC=12" in result.stdout + assert "STALE_KIMI_ARTIFACT" not in result.stdout + result.stderr assert message in result.stderr assert "failed to write Kimi verifier failure artifact" not in result.stderr assert len(score_files) == 1 @@ -736,6 +785,49 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( assert not (results_dir / "kimi_vendor_report.json").exists() assert not python_dir.exists() +def test_preclear_failure_cannot_stage_stale_provider_result(tmp_path: Path) -> None: + results_dir = tmp_path / "results" + results_dir.mkdir() + stale_result = results_dir / "results_kimi_vendor_stale.json" + stale_result.write_text('{"stale": true}\n') + work_dir = tmp_path / "work" + work_dir.mkdir() + script = r''' +source "$BENCHMARK_LIB" +rm() { return 73; } +append_lm_eval_summary() { + if [ -n "${EVAL_RESULT_DIR:-}" ]; then + echo "UNEXPECTED_STAGING" + fi + return 1 +} +unset EVAL_FRAMEWORK EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE +export MODEL=test-model +cd "$WORK_DIR" +run_eval --framework kimi-vendor --results-dir "$RESULTS_DIR" +printf 'EVAL_RC=%s\n' "$?" +printf 'EVAL_RESULT_DIR=<%s>\n' "${EVAL_RESULT_DIR:-}" +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RESULTS_DIR": str(results_dir), + "WORK_DIR": str(work_dir), + }, + text=True, + capture_output=True, + check=True, + ) + + assert "EVAL_RC=73" in result.stdout + assert "EVAL_RESULT_DIR=<>" in result.stdout + assert "UNEXPECTED_STAGING" not in result.stdout + assert "failed to remove stale eval artifact" in result.stderr + assert stale_result.is_file() + assert list(work_dir.iterdir()) == [] + _KIMI_VERIFIER_REQUIRED_FILES = { "pyproject.toml", @@ -1396,7 +1488,7 @@ def test_summary_stages_bfcl_upstream_archive_before_cleanup(tmp_path: Path) -> assert not results_dir.exists() -def test_stage_eval_artifacts_copies_verifier_outputs_only(tmp_path: Path) -> None: +def test_stage_eval_artifacts_copies_eval_outputs_only(tmp_path: Path) -> None: source_one = tmp_path / "source-one" source_two = tmp_path / "source-two" destination = tmp_path / "destination" @@ -1410,6 +1502,10 @@ def test_stage_eval_artifacts_copies_verifier_outputs_only(tmp_path: Path) -> No "bfcl_report.json", "bfcl_upstream_artifacts.tar.gz", "sample_eval.jsonl", + "agent_preds.json", + "predictions.jsonl", + "swebench_report_eval.json", + "trace.traj.json", } for filename in expected: source = source_one if filename.endswith(".json") else source_two @@ -1459,6 +1555,62 @@ def test_stage_eval_artifacts_propagates_copy_failure(tmp_path: Path) -> None: assert result.returncode == 73 +def test_stage_eval_artifacts_fails_when_no_artifacts_exist(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + script = r''' +source "$BENCHMARK_LIB" +stage_eval_artifacts "$DESTINATION" "$SOURCE" +''' + + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "DESTINATION": str(tmp_path / "destination"), + "SOURCE": str(source), + "KV_OFFLOADING": "none", + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert "no eval artifacts found to stage" in result.stderr + + +def test_summary_propagates_artifact_staging_failure(tmp_path: Path) -> None: + work_dir = tmp_path / "work" + results_dir = tmp_path / "results" + work_dir.mkdir() + results_dir.mkdir() + (results_dir / "results_eval.json").write_text("{}") + script = r''' +source "$BENCHMARK_LIB" +cp() { return 73; } +cd "$WORK_DIR" +append_lm_eval_summary +''' + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "WORK_DIR": str(work_dir), + "EVAL_RESULT_DIR": str(results_dir), + "MODEL": "test-model", + "CONC": "7", + "KV_OFFLOADING": "none", + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 73 + def test_summary_metadata_preserves_lm_eval_gsm8k_defaults(tmp_path: Path) -> None: meta = _summary_metadata(tmp_path) @@ -2105,27 +2257,59 @@ def test_multinode_eval_artifact_names_are_bounded_and_distinct() -> None: }, ] non_mtp_twin = {**targets[3], "SPEC_DECODING": "none"} + disagg_twin = {**targets[3], "DISAGG": "true"} + recipe_twin = { + **targets[3], + "EVAL_ARTIFACT_RECIPE": "0123456789abcdef", + } + suite_twin = {**targets[3], "EVAL_SUITE": "bfcl_vllm_kimi"} + conc_twin = {**targets[3], "conc-list": ["2", "16"]} def render(values: dict[str, object]) -> str: name = expression - name = re.sub( - r"\$\{\{ join\(fromJson\(inputs\.conc-list\), 'x'\) \}\}", - "x".join(values["conc-list"]), - name, - ) - for key, value in values.items(): + defaults: dict[str, object] = { + "DISAGG": "false", + "EVAL_ARTIFACT_RECIPE": "", + "EVAL_FRAMEWORK": "bfcl", + "EVAL_SUITE": "bfcl_smoke", + } + defaults["EVAL_ARTIFACT_CONC"] = hashlib.sha256( + " ".join(values["conc-list"]).encode() + ).hexdigest()[:12] + for key, value in {**defaults, **values}.items(): if key != "conc-list": name = name.replace(f"${{{{ env.{key} }}}}", str(value)) name = name.replace("${{ runner.name }}", str(values["runner.name"])) + name = name.replace("${{ github.run_attempt }}", "1") assert "${{" not in name return name - names = [render(target) for target in targets] - assert len(names) == len(set(names)) == 6 - assert render(targets[3]) != render(non_mtp_twin) + variants = [ + *targets, + non_mtp_twin, + disagg_twin, + recipe_twin, + suite_twin, + conc_twin, + ] + names = [render(target) for target in variants] + assert len(names) == len(set(names)) == len(variants) assert all(name.startswith("eval_") and len(name.encode()) <= 256 for name in names) +def test_single_node_eval_artifact_name_includes_suite_identity() -> None: + workflow = yaml.safe_load(SINGLE_NODE_WORKFLOW.read_text()) + upload = next( + step + for step in workflow["jobs"]["benchmark"]["steps"] + if step.get("name") == "Upload eval results (if any)" + ) + expression = upload["with"]["name"] + assert "EVAL_FRAMEWORK" in expression + assert "EVAL_SUITE" in expression + assert "github.run_attempt" in expression + + _GENMODE_SCRIPT = r''' source "$BENCHMARK_LIB" 2>/dev/null @@ -2285,9 +2469,9 @@ def test_fixed_eval_workflows_forward_provider_contract() -> None: reusable_workflow = yaml.safe_load(SINGLE_NODE_WORKFLOW.read_text()) assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" - assert "*_vendor_report.json" in SINGLE_NODE_WORKFLOW.read_text() - assert "bfcl_report.json" in SINGLE_NODE_WORKFLOW.read_text() - assert "bfcl_upstream_artifacts.tar.gz" in SINGLE_NODE_WORKFLOW.read_text() + assert "*_report.json" in SINGLE_NODE_WORKFLOW.read_text() + assert "*_results.jsonl" in SINGLE_NODE_WORKFLOW.read_text() + assert "*_artifacts.tar.gz" in SINGLE_NODE_WORKFLOW.read_text() assert "bfcl_vllm_minimax_m3" in SINGLE_NODE_WORKFLOW.read_text() assert "bfcl_vllm_kimi" in SINGLE_NODE_WORKFLOW.read_text() @@ -2302,9 +2486,9 @@ def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" - assert "*_vendor_report.json" in MULTINODE_WORKFLOW.read_text() - assert "bfcl_report.json" in MULTINODE_WORKFLOW.read_text() - assert "bfcl_upstream_artifacts.tar.gz" in MULTINODE_WORKFLOW.read_text() + assert "*_report.json" in MULTINODE_WORKFLOW.read_text() + assert "*_results.jsonl" in MULTINODE_WORKFLOW.read_text() + assert "*_artifacts.tar.gz" in MULTINODE_WORKFLOW.read_text() assert "bfcl_vllm_minimax_m3" in MULTINODE_WORKFLOW.read_text() assert "bfcl_vllm_kimi" in MULTINODE_WORKFLOW.read_text() @@ -2433,6 +2617,9 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( ) -> None: results_dir = tmp_path / "results" python_dir = tmp_path / "python" + results_dir.mkdir() + stale_result = results_dir / "results_bfcl_previous.json" + stale_result.write_text('{"stale": true}\n') script = r""" source "$BENCHMARK_LIB" export EVAL_SUITE=bfcl_vllm_kimi @@ -2478,7 +2665,7 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( assert result.returncode == 124 assert "EVAL_RC=124" in output - assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/bfcl_eval.py'}>" in output + assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/bfcl_adapter.py'}>" in output assert "ADAPTER_ARG=" in output assert f"ADAPTER_ARG=<{results_dir}>" in output assert "ADAPTER_ARG=<--integration-error>" in output @@ -2490,6 +2677,7 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( assert output.count(f"STAGED=<{results_dir}>") == 1 assert (results_dir / "bfcl_report.json").exists() assert (results_dir / "results_bfcl.json").exists() + assert not stale_result.exists() assert "failed to write BFCL failure artifact" not in output assert "UNEXPECTED_SYSTEM_PYTHON" not in output assert not (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() @@ -2610,7 +2798,7 @@ def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( assert result.returncode == 0, result.stderr for value in ( - str(REPO_ROOT / "utils/evals/bfcl_eval.py"), + str(REPO_ROOT / "utils/evals/bfcl_adapter.py"), "--base-url", "http://127.0.0.1:9999/v1", "--api-key", diff --git a/utils/evals/validate_scores.py b/utils/evals/validate_scores.py index bf4b391728..9450892f55 100644 --- a/utils/evals/validate_scores.py +++ b/utils/evals/validate_scores.py @@ -88,6 +88,21 @@ def invalid_effective_count(data: dict, task: str) -> tuple[bool, object]: ) return invalid, effective +def metric_prefixes(data: dict, task: str, override: str | None) -> tuple[str, ...]: + """Resolve score metric prefixes from an explicit override or task config.""" + if override is not None: + return (override,) + task_config = data.get("configs", {}).get(task, {}) + metric_list = task_config.get("metric_list", []) + declared = tuple( + f"{item['metric']}," + for item in metric_list + if isinstance(item, dict) + and isinstance(item.get("metric"), str) + and item["metric"] + ) + return declared or ("exact_match,",) + def integration_error_message(error: object) -> str: """Render the structured integration error fields for a direct failure.""" @@ -226,8 +241,9 @@ def main() -> int: help="Override the detected model prefix (default: read from meta_env.json / $MODEL_PREFIX)", ) parser.add_argument( - "--metric-prefix", default="exact_match,", - help="Only check metrics whose name starts with this prefix (default: 'exact_match,')", + "--metric-prefix", + default=None, + help="Override task-config metric selection with one metric prefix", ) parser.add_argument( "--results-glob", default="results*.json", @@ -325,8 +341,9 @@ def main() -> int: failed = True continue min_score, source = resolve_threshold(config, prefix, task, args.min_score) + prefixes = metric_prefixes(data, task, args.metric_prefix) for name, val in metrics.items(): - if not name.startswith(args.metric_prefix) or "stderr" in name: + if not name.startswith(prefixes) or "stderr" in name: continue if not isinstance(val, (int, float)): continue @@ -343,7 +360,8 @@ def main() -> int: ) if checked == 0: - print("WARN: no metrics matched prefix '{}'".format(args.metric_prefix), file=sys.stderr) + selector = args.metric_prefix or "declared task metrics" + print(f"WARN: no metrics matched {selector!r}", file=sys.stderr) return 1 if (failed or checked == 0) else 0 From f020a8e27aa35a21cfe77588f90f92c4b102eaa6 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:09:26 -0500 Subject: [PATCH 57/99] fix: enable tool parsing for kimi k3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为 Kimi K3 启用工具调用与推理解析。 --- benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh b/benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh index 1212309a21..86b07e5f2d 100755 --- a/benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh +++ b/benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh @@ -269,6 +269,9 @@ VLLM_CMD=( --max-num-batched-tokens 16384 --trust-remote-code --language-model-only + --enable-auto-tool-choice + --tool-call-parser kimi_k3 + --reasoning-parser kimi_k3 --load-format fastsafetensors --moe-backend auto --no-enable-flashinfer-autotune From d070e3b1b55fd6b05f942ff570240b410920bd41 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:41:34 -0500 Subject: [PATCH 58/99] fix: harden tool use deployment verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:强化工具调用部署验证,补齐解析器配置、失败分类、重试机制和可复现的覆盖矩阵。 --- .github/workflows/e2e-tests.yml | 9 ++ .../gb200-fp4/agentic/agg-dep4-agentic.yaml | 13 +- .../agentic/agg-dep4-vllm-simple-agentic.yaml | 13 +- .../gb200-fp4/agentic/agg-dep8-agentic.yaml | 13 +- .../gb200-fp4/agentic/agg-tp4-agentic.yaml | 5 + .../agentic/agg-tp4-vllm-simple-agentic.yaml | 13 +- .../disagg-1p1d-dep8-dep4-agentic.yaml | 13 +- .../agentic/minimaxm3_fp4_mi355x_mtp.sh | 1 + runners/test_slurm_utils.py | 28 ++++ utils/collect_eval_results.py | 99 ++++++++++--- utils/evals/EVALS.md | 73 +++++++++- utils/evals/kimi_vendor_eval.py | 88 +++++++++++- utils/evals/minimax_m3_full_eval.py | 63 ++++++--- utils/evals/test_kimi_vendor_eval.py | 54 ++++++- utils/evals/test_minimax_m3_full_eval.py | 63 +++++++++ utils/evals/test_run_eval_dispatch.py | 6 +- utils/matrix_logic/generate_sweep_configs.py | 82 +++++++++++ .../test_generate_sweep_configs.py | 133 +++++++++++++++++- utils/process_changelog.py | 65 +-------- utils/test_collect_eval_results.py | 70 ++++++++- 20 files changed, 780 insertions(+), 124 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 38b3a2fe16..f94026dd84 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -263,6 +263,15 @@ jobs: echo "generate-cli-command is required outside trusted changelog dispatch mode" >&2 exit 1 fi + if [ "$TRIM_CONC" = "true" ]; then + GENERATE_COMMAND+=" --trim-conc" + fi + if [ "$ALL_EVALS" = "true" ]; then + GENERATE_COMMAND+=" --all-evals" + fi + if [ "$EVALS_ONLY" = "true" ]; then + GENERATE_COMMAND+=" --evals-only" + fi CONFIG_JSON=$(uv run --no-project --with pydantic --with pyyaml --python 3.12 \ "${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py" \ $GENERATE_COMMAND) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-agentic.yaml index 4686d003fc..faf3d780be 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-agentic.yaml @@ -15,7 +15,18 @@ frontend: type: dynamo enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } - args: { router-mode: "kv", router-kv-events: true, router-reset-states: true, router-temperature: "0", router-session-affinity-ttl-secs: 14400, kv-cache-block-size: 128 } + args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true + router-mode: "kv" + router-kv-events: true + router-reset-states: true + router-temperature: "0" + router-session-affinity-ttl-secs: 14400 + kv-cache-block-size: 128 backend: type: vllm connector: null diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-vllm-simple-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-vllm-simple-agentic.yaml index 528e08b835..3d81cbacf0 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-vllm-simple-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep4-vllm-simple-agentic.yaml @@ -15,7 +15,18 @@ frontend: type: dynamo enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } - args: { router-mode: "kv", router-kv-events: true, router-reset-states: true, router-temperature: "0", router-session-affinity-ttl-secs: 14400, kv-cache-block-size: 128 } + args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true + router-mode: "kv" + router-kv-events: true + router-reset-states: true + router-temperature: "0" + router-session-affinity-ttl-secs: 14400 + kv-cache-block-size: 128 backend: type: vllm connector: null diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep8-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep8-agentic.yaml index b8b4c33411..c056c101dd 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep8-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-dep8-agentic.yaml @@ -15,7 +15,18 @@ frontend: type: dynamo enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } - args: { router-mode: "kv", router-kv-events: true, router-reset-states: true, router-temperature: "0", router-session-affinity-ttl-secs: 14400, kv-cache-block-size: 128 } + args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true + router-mode: "kv" + router-kv-events: true + router-reset-states: true + router-temperature: "0" + router-session-affinity-ttl-secs: 14400 + kv-cache-block-size: 128 backend: type: vllm connector: null diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-agentic.yaml index b74dd665b4..6403d69e5b 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-agentic.yaml @@ -30,6 +30,11 @@ frontend: enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true router-mode: "kv" router-kv-events: true router-reset-states: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-vllm-simple-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-vllm-simple-agentic.yaml index 1d62599edd..41a1835083 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-vllm-simple-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/agg-tp4-vllm-simple-agentic.yaml @@ -21,7 +21,18 @@ frontend: type: dynamo enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } - args: { router-mode: "kv", router-kv-events: true, router-reset-states: true, router-temperature: "0", router-session-affinity-ttl-secs: 14400, kv-cache-block-size: 128 } + args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true + router-mode: "kv" + router-kv-events: true + router-reset-states: true + router-temperature: "0" + router-session-affinity-ttl-secs: 14400 + kv-cache-block-size: 128 backend: type: vllm diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/disagg-1p1d-dep8-dep4-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/disagg-1p1d-dep8-dep4-agentic.yaml index 7c2343f404..1a798085b9 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/disagg-1p1d-dep8-dep4-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3/gb200-fp4/agentic/disagg-1p1d-dep8-dep4-agentic.yaml @@ -23,7 +23,18 @@ frontend: type: dynamo enable_multiple_frontends: false env: { DYN_TCP_REQUEST_TIMEOUT: "60" } - args: { router-mode: "kv", router-kv-events: true, router-reset-states: true, router-temperature: "0", router-session-affinity-ttl-secs: 14400, kv-cache-block-size: 128 } + args: + dyn-chat-processor: "vllm" + trust-remote-code: true + tool-call-parser: "minimax_m3" + reasoning-parser: "minimax_m3" + enable-auto-tool-choice: true + router-mode: "kv" + router-kv-events: true + router-reset-states: true + router-temperature: "0" + router-session-affinity-ttl-secs: 14400 + kv-cache-block-size: 128 backend: type: vllm connector: null diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh index 11135a0583..d30656dda7 100644 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh @@ -215,6 +215,7 @@ VLLM_CMD=( --moe-backend aiter --kv-cache-dtype fp8 --tool-call-parser minimax_m3 + --reasoning-parser minimax_m3 --enable-auto-tool-choice --default-chat-template-kwargs '{"thinking_mode":"enabled"}' --max-num-seqs "$((2 * CONC))" diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 3a16511b21..e5d917ebdb 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -332,6 +332,34 @@ def test_gb200_dynamo_kimi_recipes_configure_tool_parser() -> None: assert config["dyn-tool-call-parser"] == "kimi_k3" +def test_gb200_dynamo_minimax_recipes_configure_frontend_tool_parser() -> None: + recipe_dir = ( + REPO_ROOT + / "benchmarks/multi_node/srt-slurm-recipes/vllm/minimax-m3" + / "gb200-fp4/agentic" + ) + recipe_paths = sorted(recipe_dir.glob("*.yaml")) + + assert len(recipe_paths) == 6 + for recipe_path in recipe_paths: + recipe = yaml.safe_load(recipe_path.read_text()) + args = recipe["frontend"]["args"] + assert args["dyn-chat-processor"] == "vllm", recipe_path + assert args["tool-call-parser"] == "minimax_m3", recipe_path + assert args["reasoning-parser"] == "minimax_m3", recipe_path + assert args["enable-auto-tool-choice"] is True, recipe_path + + +def test_mi355_minimax_launcher_configures_reasoning_parser() -> None: + launcher = ( + REPO_ROOT + / "benchmarks/single_node/agentic/minimaxm3_fp4_mi355x_mtp.sh" + ).read_text() + + assert "--tool-call-parser minimax_m3" in launcher + assert "--reasoning-parser minimax_m3" in launcher + assert "--enable-auto-tool-choice" in launcher + def test_dynamo_sglang_agentic_recipes_parse_tools_at_frontend() -> None: recipe_roots = ( diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index dc48d9648b..0f86715632 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -167,21 +167,69 @@ def extract_lm_metrics(json_path: Path) -> List[Dict[str, Any]]: - Values from results[task][metric,filter] """ data = load_json(json_path) or {} - if 'integration_error' in data: - return [] results = data.get('results', {}) - configs = data.get('configs', {}) + raw_configs = data.get('configs', {}) + configs = raw_configs if isinstance(raw_configs, dict) else {} - if not results: + if not isinstance(results, dict) or not results: return [] extracted = [] for task in results.keys(): - if has_invalid_effective_count(data, task): - continue task_results = results[task] - task_config = configs.get(task, {}) + raw_task_config = configs.get(task, {}) + task_config = ( + raw_task_config if isinstance(raw_task_config, dict) else {} + ) + raw_metadata = task_config.get('metadata', {}) + metadata = raw_metadata if isinstance(raw_metadata, dict) else {} + model = data.get('model_name') or metadata.get('model') + + sample_counts = data.get('n-samples') + raw_task_samples = ( + sample_counts.get(task) + if isinstance(sample_counts, dict) + else None + ) + n_eff = ( + raw_task_samples.get('effective') + if isinstance(raw_task_samples, dict) + else None + ) + invalid_effective_count = has_invalid_effective_count(data, task) + integration_error = data.get('integration_error') + if integration_error is None and invalid_effective_count: + integration_error = { + 'type': 'InvalidEffectiveSampleCount', + 'message': f'invalid effective sample count: {n_eff!r}', + } + if integration_error is None and not isinstance(task_results, dict): + integration_error = { + 'type': 'InvalidTaskResults', + 'message': f'invalid task results for {task!r}', + } + if integration_error is not None: + if not isinstance(integration_error, dict): + integration_error = { + 'type': 'IntegrationError', + 'message': str(integration_error), + } + extracted.append({ + 'task': task, + 'strict': None, + 'strict_se': None, + 'flex': None, + 'flex_se': None, + 'accuracy': None, + 'accuracy_se': None, + 'n_eff': 0 if invalid_effective_count else n_eff, + 'model': model, + 'source': str(json_path), + 'infrastructure_success': False, + 'integration_error': integration_error, + }) + continue # Base metric: from config's metric_list metric_list = task_config.get('metric_list', []) @@ -224,12 +272,6 @@ def get_val_se(filter_name: str) -> Tuple[Optional[float], Optional[float]]: # N-samples (effective count) n_eff = data.get('n-samples', {}).get(task, {}).get('effective') - # Model name - model = ( - data.get('model_name') - or task_config.get('metadata', {}).get('model') - ) - extracted.append({ 'task': task, 'strict': strict_val, @@ -240,7 +282,9 @@ def get_val_se(filter_name: str) -> Tuple[Optional[float], Optional[float]]: 'accuracy_se': accuracy_se, 'n_eff': n_eff, 'model': model, - 'source': str(json_path) + 'source': str(json_path), + 'infrastructure_success': True, + 'integration_error': None, }) return extracted @@ -332,6 +376,8 @@ def build_row(meta: Dict[str, Any], m: Dict[str, Any]) -> Dict[str, Any]: 'em_flexible_se': m.get('flex_se'), 'n_eff': m.get('n_eff'), 'source': m.get('source'), + 'infrastructure_success': m.get('infrastructure_success', True), + 'integration_error': m.get('integration_error'), } if 'eval_suite' in meta: @@ -383,6 +429,9 @@ def collect_eval_rows(root: Path) -> List[Dict[str, Any]]: metrics_list = extract_lm_metrics(lm_path) for metrics in metrics_list: + if metrics.get('infrastructure_success') is False: + rows.append(build_row(row_meta, metrics)) + continue primary_score = next( ( metrics.get(name) @@ -391,12 +440,30 @@ def collect_eval_rows(root: Path) -> List[Dict[str, Any]]: ), None, ) - if ( + invalid_primary_score = ( isinstance(primary_score, bool) or not isinstance(primary_score, (int, float)) or not math.isfinite(primary_score) or not 0.0 <= primary_score <= 1.0 - ): + ) + if invalid_primary_score: + failed_metrics = { + **metrics, + 'strict': None, + 'strict_se': None, + 'accuracy': None, + 'accuracy_se': None, + 'flex': None, + 'flex_se': None, + 'infrastructure_success': False, + 'integration_error': { + 'type': 'InvalidPrimaryScore', + 'message': ( + f'invalid primary score: {primary_score!r}' + ), + }, + } + rows.append(build_row(row_meta, failed_metrics)) continue rows.append(build_row(row_meta, metrics)) return rows diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index df4d96f5a0..287660b2fe 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -21,10 +21,12 @@ Generator eval modes: - Default: throughput plus the selected eval subset. - `--no-evals`: throughput only. - `--evals-only`: selected evals only. -- `--all-evals`: every fixed-sequence eval only. This is equivalent to - `--evals-only --all-evals`. Multi-node topologies run all `conc-list` values - sequentially on one engine. Agentic-coding configs are included and run - GSM8K (they are excluded only from the default, non-eval sweep). +- `--all-evals`: every eligible fixed-sequence and agentic eval. This is + equivalent to `--evals-only --all-evals`. Multi-node fixed-sequence + topologies run all `conc-list` values sequentially on one engine. +- `--trim-conc`: after eval selection, retain the minimum concurrency for each + single-node or multi-node deployment shape and move that shape's selected eval + to the retained row. This is the deployment smoke mode, not a throughput sweep. Changelog entries use `evals-only: true` and `all-evals: true`. The `all-evals` setting implies eval-only there. On PRs, the same names are modifier labels: @@ -34,6 +36,52 @@ suppresses it. Modifier runs cannot be reused. Deduplication is scenario-aware: fixed-sequence coverage does not suppress agentic coverage, and `all-evals` wins over default eval coverage. +### Tool-use support contract + +The tool-use adapters are backend-independent clients of the local +OpenAI-compatible endpoint. The deployment-smoke target set contains every +generated Kimi K3 and MiniMax M3 agentic configuration in the NVIDIA and AMD +master configs, including their single-node and multi-node vLLM and +Dynamo-vLLM recipes. A configuration is verified only when the current PR head +launches its tool-aware endpoint, the matching vendor smoke and `bfcl_smoke` +complete their expected sample counts, and the native and +`inferencex-eval-v1` artifacts are collected without `integration_error`. + +Infrastructure support does not mean every model must pass every quality +threshold. A completed result with a positive effective sample count can score +below its threshold and fail the quality gate without being a deployment +failure. Missing parser support, transport errors, timeouts, malformed output, +missing samples, and missing artifacts are infrastructure failures. + +Generator coverage and static parser checks do not prove a live backend. Before +claiming complete deployment support, run both smoke suites on every row from +the matrices below at the current PR head. Run each full vendor or BFCL +model-quality suite on at least one matching deployment; these longer suites do +not need to repeat on every equivalent parser topology. + +Generate the complete deployment-smoke matrices with: + +```bash +uv run --no-project --with pydantic --with pyyaml --python 3.12 \ + python utils/matrix_logic/generate_sweep_configs.py full-sweep \ + --config-files configs/nvidia-master.yaml configs/amd-master.yaml \ + --model-prefix kimik3 \ + --scenario-type agentic-coding \ + --evals-only --all-evals --trim-conc + +uv run --no-project --with pydantic --with pyyaml --python 3.12 \ + python utils/matrix_logic/generate_sweep_configs.py full-sweep \ + --config-files configs/nvidia-master.yaml configs/amd-master.yaml \ + --model-prefix minimaxm3 \ + --scenario-type agentic-coding \ + --evals-only --all-evals --trim-conc +``` + +Run each generated matrix with the matching vendor smoke and `bfcl_smoke`. +The full Kimi, MiniMax, and BFCL suites use the same endpoint and artifact +paths, but are diagnostic model-quality campaigns rather than a replacement +for the per-topology deployment smoke. + ### Artifact reuse Default full sweeps may reuse their eval subset. Source coverage is @@ -48,8 +96,8 @@ runner. Existing jobs continue to use lm-eval with GSM8K by default. The default eval framework is [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (`lm-eval`). Agentic eval-only matrix jobs inherit this default and therefore run the same GSM8K task as 8k1k. Explicit agentic runs can still select SWE-bench. -The Phase 1 Kimi smoke is opt-in. It supports single-node jobs and Kimi K3 -aggregate H200, B200, and GB200 srt-slurm jobs. Select +The Kimi smoke is opt-in and applies to supported models exposing a +tool-aware OpenAI-compatible chat-completions API. Select `eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema` on `e2e-tests.yml`, or invoke it from the repository root after a server is ready: @@ -485,7 +533,9 @@ gh run download --repo SemiAnalysisAI/InferenceX -n eval_results_all -D # View eval summary cat ./evals/agg_eval_all.json | jq -r ' - .[] | [.hw, .framework, .precision, .tp, .conc, .task, (.score * 100 | round | . / 100)] + .[] | [.hw, .framework, .precision, .tp, .conc, .task, + (if .infrastructure_success then ((.score * 100 | round) / 100) + else .integration_error.type end)] | @tsv' | column -t # Filter to specific hardware @@ -502,6 +552,15 @@ cat ./evals/agg_eval_all.json | jq '[.[] | select(.hw == "B200")]' | `n_eff` | Number of samples evaluated | | `task` | Eval task name (e.g., `gsm8k`) | | `eval_suite` | Explicit suite identity used for collection and artifact reuse | +| `infrastructure_success` | `false` when setup, transport, timeout, sample-count, or score validation failed | +| `integration_error` | Structured infrastructure failure type and message, otherwise `null` | + +Collection retains the latest attempt for each artifact or batched concurrency. +Raw compatibility artifacts encode infrastructure failures with `score: 0`, +`n_eff: 0`, and `integration_error`. Aggregation preserves the failure row but +sets `score: null` and `infrastructure_success: false`, so dashboards cannot +mistake an endpoint failure for measured model quality. An older successful +attempt cannot replace a newer failed retry. ### Environment variables diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 9007393f98..9d95462a19 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -5,6 +5,7 @@ import argparse import json +import re import subprocess import sys from collections.abc import Mapping, Sequence @@ -26,6 +27,8 @@ RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" +ENDPOINT_REJECTION_RE = re.compile(r"(?im)tool schema rejected:") + def prepare_compatibility_path(output_dir: Path) -> Path: """Remove stale projections and return a timestamped collector artifact path.""" @@ -100,6 +103,24 @@ def _expected_total(task_name: str) -> int: except KeyError as exc: raise ValueError(f"unsupported Kimi task: {task_name}") from exc +def _endpoint_rejection_messages(report: Any) -> list[str]: + """Return upstream failures rejected before argument-schema validation.""" + root = _mapping(report, "report") + results = root.get("results") + if not isinstance(results, list): + raise ValueError("report.results must be an array") + messages: list[str] = [] + for index, result in enumerate(results): + record = _mapping(result, f"report.results[{index}]") + message = record.get("message") + if ( + record.get("status") == "failed" + and isinstance(message, str) + and ENDPOINT_REJECTION_RE.search(message) + ): + messages.append(message) + return messages + def _project_report( model: str, @@ -274,6 +295,38 @@ def _compatibility_result( } return result +def _write_native_failure( + path: Path, + *, + model: str, + task_name: str, + error: BaseException, +) -> None: + """Write a native diagnostic envelope when upstream cannot produce one.""" + path.write_text( + json.dumps( + { + "generated_at": datetime.now(timezone.utc).isoformat(), + "model": model, + "task": task_name, + "completed": False, + "summary": { + "total": 0, + "expected_total": _expected_total(task_name), + "by_status": {}, + }, + "results": [], + "integration_error": { + "type": type(error).__name__, + "message": str(error), + }, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + def _write_compatibility(path: Path, result: Mapping[str, Any]) -> None: path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") @@ -327,13 +380,27 @@ def run_evaluation( report, task_name=task_name, ) + endpoint_rejections = _endpoint_rejection_messages(report) valid_outcome = (subprocess_rc == 0 and all_passed) or ( subprocess_rc == 1 and not all_passed ) completed_successfully = subprocess_rc == 0 and all_passed - if task_name == FULL_TASK_NAME and valid_outcome: + if endpoint_rejections: + integration_error = RuntimeError( + "upstream verifier reported " + f"{len(endpoint_rejections)} endpoint request or response failure(s)" + ) + compatibility = _compatibility_result( + model, + 0.0, + task_name=task_name, + n_samples=0, + integration_error=integration_error, + ) + completed_successfully = False + elif task_name == FULL_TASK_NAME and valid_outcome: completed_successfully = True - if not valid_outcome: + elif not valid_outcome: integration_error = RuntimeError( f"upstream verifier exited with code {subprocess_rc}" ) @@ -354,6 +421,13 @@ def run_evaluation( n_samples=0, integration_error=exc, ) + if not native_report.exists(): + _write_native_failure( + native_report, + model=model, + task_name=task_name, + error=exc, + ) finally: try: _write_compatibility(compatibility_path, compatibility) @@ -411,7 +485,13 @@ def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) if args.integration_error is not None: args.output_dir.mkdir(parents=True, exist_ok=True) - (args.output_dir / NATIVE_REPORT_FILENAME).unlink(missing_ok=True) + error = RuntimeError(args.integration_error) + _write_native_failure( + args.output_dir / NATIVE_REPORT_FILENAME, + model=args.model, + task_name=args.task_name, + error=error, + ) _write_compatibility( prepare_compatibility_path(args.output_dir), _compatibility_result( @@ -419,7 +499,7 @@ def main(argv: Sequence[str] | None = None) -> int: 0.0, task_name=args.task_name, n_samples=0, - integration_error=RuntimeError(args.integration_error), + integration_error=error, ), ) return 0 diff --git a/utils/evals/minimax_m3_full_eval.py b/utils/evals/minimax_m3_full_eval.py index 0fdf9f6c86..98af3c2e94 100755 --- a/utils/evals/minimax_m3_full_eval.py +++ b/utils/evals/minimax_m3_full_eval.py @@ -9,6 +9,7 @@ import math import os import subprocess +import time import urllib.error import urllib.parse import urllib.request @@ -44,6 +45,8 @@ } MAX_SOURCE_BYTES = 16 * 1024 * 1024 DOWNLOAD_TIMEOUT_SECONDS = 60 +DOWNLOAD_ATTEMPTS = 3 +DOWNLOAD_RETRY_DELAY_SECONDS = 3 UPSTREAM_TIMEOUT_SECONDS = 7 * 60 * 60 Runner = Callable[..., subprocess.CompletedProcess[Any]] @@ -96,28 +99,44 @@ def _fetch_source(relative_path: str) -> bytes: headers={"Accept": "application/octet-stream"}, method="GET", ) - try: - with _NO_REDIRECT_OPENER.open( - request, timeout=DOWNLOAD_TIMEOUT_SECONDS - ) as response: - status = getattr(response, "status", None) - if status != 200 or response.geturl() != url: - raise FullSuiteError( - f"unexpected response for pinned source {relative_path}: " - f"status={status!r}, url={response.geturl()!r}" - ) - declared_size = response.headers.get("Content-Length") - if declared_size is not None and int(declared_size) > MAX_SOURCE_BYTES: - raise FullSuiteError( - f"pinned source {relative_path} exceeds the size limit" - ) - content = response.read(MAX_SOURCE_BYTES + 1) - except (OSError, ValueError, urllib.error.URLError) as exc: - raise FullSuiteError( - f"failed to download pinned source {relative_path}: {exc}" - ) from exc - verify_source_content(relative_path, content) - return content + last_error: BaseException | None = None + for attempt in range(1, DOWNLOAD_ATTEMPTS + 1): + try: + with _NO_REDIRECT_OPENER.open( + request, timeout=DOWNLOAD_TIMEOUT_SECONDS + ) as response: + status = getattr(response, "status", None) + if status != 200 or response.geturl() != url: + raise FullSuiteError( + f"unexpected response for pinned source {relative_path}: " + f"status={status!r}, url={response.geturl()!r}" + ) + declared_size = response.headers.get("Content-Length") + if ( + declared_size is not None + and int(declared_size) > MAX_SOURCE_BYTES + ): + raise FullSuiteError( + f"pinned source {relative_path} exceeds the size limit" + ) + content = response.read(MAX_SOURCE_BYTES + 1) + except ( + FullSuiteError, + OSError, + ValueError, + urllib.error.URLError, + ) as exc: + last_error = exc + if attempt < DOWNLOAD_ATTEMPTS: + time.sleep(DOWNLOAD_RETRY_DELAY_SECONDS) + continue + break + verify_source_content(relative_path, content) + return content + raise FullSuiteError( + f"failed to download pinned source {relative_path} after " + f"{DOWNLOAD_ATTEMPTS} attempts: {last_error}" + ) from last_error def _validate_sample(content: bytes) -> None: diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index 6cf352f9d7..d8cb08707b 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -258,7 +258,12 @@ def test_full_report_projects_all_mode_records_and_defers_quality_gating( monkeypatch: pytest.MonkeyPatch, ) -> None: output_dir = tmp_path / "output" - native_bytes = json.dumps(_full_report(failed_records=1)).encode() + report = _full_report(failed_records=1) + report["results"][0]["message"] = ( + "AssertionError: TestSchema:1 [non-stream] (all) " + "arguments validation failed: 'bad' is not valid" + ) + native_bytes = json.dumps(report).encode() invocation: dict[str, Any] = {} def fake_run( @@ -290,6 +295,46 @@ def fake_run( assert "integration_error" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes +def test_full_report_classifies_endpoint_failures_as_integration_errors( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + report = _full_report(failed_records=1) + report["results"][0]["message"] = ( + "AssertionError: TestSchema:1 [non-stream] (all) tool schema rejected: " + "Data returned by API invalid for expected schema." + ) + native_bytes = json.dumps(report).encode() + + def fake_run( + command: list[str], *, cwd: Path, check: bool, timeout: int + ) -> SimpleNamespace: + Path(command[command.index("--tool-json-report") + 1]).write_bytes( + native_bytes + ) + return SimpleNamespace(returncode=1) + + monkeypatch.setattr(kve.subprocess, "run", fake_run) + output_dir = tmp_path / "output" + + assert not kve.run_evaluation( + verifier_dir=tmp_path, + base_url="http://localhost/v1", + api_key="EMPTY", + model="model-a", + output_dir=output_dir, + task_name=kve.FULL_TASK_NAME, + timeout_seconds=kve.FULL_TIMEOUT_SECONDS, + ) + projected = _result(output_dir) + assert _score(output_dir, kve.FULL_TASK_NAME) == 0.0 + assert _n_eff(output_dir, kve.FULL_TASK_NAME) == 0 + assert projected["integration_error"]["type"] == "RuntimeError" + assert "endpoint request or response failure" in projected[ + "integration_error" + ]["message"] + assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes + def test_full_report_rejects_incomplete_modes( tmp_path: Path, @@ -396,7 +441,7 @@ def fail_collection(*args: Any, **kwargs: Any) -> SimpleNamespace: output_dir=output_dir, ) assert _score(output_dir) == 0.0 - assert not native_report.exists() + assert json.loads(native_report.read_text())["completed"] is False assert foreign_result.exists() @@ -422,7 +467,10 @@ def test_cli_setup_failure_writes_zero_score_artifact(tmp_path: Path) -> None: == 0 ) projected = _result(output_dir) - assert not (output_dir / kve.NATIVE_REPORT_FILENAME).exists() + native = json.loads((output_dir / kve.NATIVE_REPORT_FILENAME).read_text()) + assert native["completed"] is False + assert native["summary"]["expected_total"] == 2 + assert native["integration_error"]["message"] == "checkout failed" assert _score(output_dir) == 0.0 assert projected["integration_error"]["message"] == "checkout failed" assert _n_eff(output_dir) == 0 diff --git a/utils/evals/test_minimax_m3_full_eval.py b/utils/evals/test_minimax_m3_full_eval.py index 369062e217..e3ef5c485a 100644 --- a/utils/evals/test_minimax_m3_full_eval.py +++ b/utils/evals/test_minimax_m3_full_eval.py @@ -72,6 +72,69 @@ def test_prepared_source_tree_requires_every_pinned_byte( "3ead102af0f888acc95867b3a9916942524b02f4f64931f020a1bfb4fee9aae2" ) +def test_fetch_source_retries_transient_network_failures( + monkeypatch: pytest.MonkeyPatch, +) -> None: + content = b"pinned source" + monkeypatch.setattr( + full, + "REQUIRED_SOURCE_SHA256", + {"verify.py": hashlib.sha256(content).hexdigest()}, + ) + sleeps: list[int] = [] + + class Response: + status = 200 + headers: dict[str, str] = {} + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def geturl(self) -> str: + return full.source_url("verify.py") + + def read(self, _: int) -> bytes: + return content + + class Opener: + calls = 0 + + def open(self, request, *, timeout): + self.calls += 1 + if self.calls < full.DOWNLOAD_ATTEMPTS: + raise full.urllib.error.URLError("transient") + return Response() + + opener = Opener() + monkeypatch.setattr(full, "_NO_REDIRECT_OPENER", opener) + monkeypatch.setattr(full.time, "sleep", sleeps.append) + + assert full._fetch_source("verify.py") == content + assert opener.calls == full.DOWNLOAD_ATTEMPTS + assert sleeps == [full.DOWNLOAD_RETRY_DELAY_SECONDS] * 2 + + +def test_fetch_source_reports_exhausted_retries( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class Opener: + calls = 0 + + def open(self, request, *, timeout): + self.calls += 1 + raise full.urllib.error.URLError("offline") + + opener = Opener() + monkeypatch.setattr(full, "_NO_REDIRECT_OPENER", opener) + monkeypatch.setattr(full.time, "sleep", lambda _: None) + + with pytest.raises(full.FullSuiteError, match="after 3 attempts"): + full._fetch_source("verify.py") + assert opener.calls == full.DOWNLOAD_ATTEMPTS + def test_verifier_command_is_one_102_row_run_with_fixed_m3_settings( tmp_path: Path, diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 4997af0b76..1ff9be2afb 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -782,7 +782,11 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( == 0.0 ) assert score_result["integration_error"]["message"] == message - assert not (results_dir / "kimi_vendor_report.json").exists() + native_result = json.loads( + (results_dir / "kimi_vendor_report.json").read_text() + ) + assert native_result["completed"] is False + assert native_result["integration_error"]["message"] == message assert not python_dir.exists() def test_preclear_failure_cannot_stage_stale_provider_result(tmp_path: Path) -> None: diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 815eefb01a..ad78ecd8b4 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -43,6 +43,76 @@ def seq_len_to_str(isl: int, osl: int) -> str: """ return seq_len_itos.get((isl, osl), f"{isl}_{osl}") +def freeze_config_value(value): + """Convert JSON-shaped config values into deterministic hashable values.""" + if isinstance(value, dict): + return tuple( + sorted((key, freeze_config_value(item)) for key, item in value.items()) + ) + if isinstance(value, list): + return tuple(freeze_config_value(item) for item in value) + return value + + +def trim_conc(entries: list[dict]) -> list[dict]: + """Retain the lowest concurrency for each generated deployment shape. + + Entries are grouped by every non-eval field except ``conc`` and the + generated ``exp-name``. Multi-node rows may encode concurrency either as a + list within one row or as one-row list chunks; both representations collapse + to one row whose ``conc`` and dispatch-facing ``eval-conc`` use the minimum. + """ + ignored_fields = { + "conc", + "exp-name", + "run-eval", + "eval-only", + "eval-conc", + "eval-all-concs", + } + groups: dict[tuple, list[int]] = {} + out: list[dict] = [] + + def minimum_concurrency(entry: dict): + conc = entry["conc"] + return min(conc) if isinstance(conc, list) else conc + + for source_entry in entries: + entry = source_entry + conc = entry.get("conc") + if entry.get("prefill") is not None and isinstance(conc, list) and conc: + minimum_conc = min(conc) + if len(conc) > 1 or entry.get("eval-conc") != minimum_conc: + entry = {**entry, "conc": [minimum_conc]} + if "eval-conc" in entry: + entry["eval-conc"] = minimum_conc + + key = tuple( + sorted( + (key, freeze_config_value(value)) + for key, value in entry.items() + if key not in ignored_fields + ) + ) + groups.setdefault(key, []).append(len(out)) + out.append(entry) + + drop: set[int] = set() + for indices in groups.values(): + keep = min(indices, key=lambda index: minimum_concurrency(out[index])) + kept_entry = out[keep] + if any(out[index].get("run-eval") is True for index in indices): + kept_entry = {**kept_entry, "run-eval": True} + if kept_entry.get("prefill") is not None: + kept_entry["eval-conc"] = minimum_concurrency(kept_entry) + if any( + out[index].get("eval-all-concs") is True for index in indices + ): + kept_entry["eval-all-concs"] = True + out[keep] = kept_entry + drop.update(index for index in indices if index != keep) + return [entry for index, entry in enumerate(out) if index not in drop] + def runner_labels(runner_data: dict) -> dict: """Return runner scheduling labels.""" @@ -1269,6 +1339,14 @@ def main(): 'Can be combined with --evals-only; used alone, it also emits eval-only jobs.' ) ) + parent_parser.add_argument( + '--trim-conc', + action='store_true', + help=( + 'Trim each generated deployment shape to its minimum concurrency ' + 'after applying eval selection.' + ) + ) parent_parser.add_argument( '--runner-node-filter', required=False, @@ -1437,12 +1515,16 @@ def main(): else: parser.error(f"Unknown command: {args.command}") + # Apply the existing eval policy first, then expand it when requested. if not args.no_evals: matrix_values = mark_eval_entries(matrix_values, include_agentic=args.evals_only or args.all_evals) if args.all_evals: matrix_values = mark_all_eval_entries(matrix_values) + if args.trim_conc: + matrix_values = trim_conc(matrix_values) + if args.evals_only or args.all_evals: matrix_values = [e for e in matrix_values if e.get(Fields.RUN_EVAL.value, False)] for entry in matrix_values: diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index bc050c57ea..a843f06ff4 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -1,7 +1,11 @@ """Comprehensive tests for generate_sweep_configs.py""" -import pytest import argparse import copy +import hashlib +import json +from pathlib import Path + +import pytest from generate_sweep_configs import ( MIN_EVAL_CONC, seq_len_stoi, @@ -13,6 +17,7 @@ mark_all_eval_entries, apply_node_type_defaults, expand_config_keys, + trim_conc, ) @@ -1905,6 +1910,132 @@ def test_all_evals_composes_with_evals_only( assert all(entry['run-eval'] is True for entry in result) assert all(entry['eval-only'] is True for entry in result) + def test_trim_conc_reduces_generated_eval_matrix( + self, + monkeypatch, + sample_single_node_config, + sample_runner_config, + ): + import sys + import generate_sweep_configs + + monkeypatch.setattr( + generate_sweep_configs, + 'load_config_files', + lambda _: sample_single_node_config, + ) + monkeypatch.setattr( + generate_sweep_configs, + 'load_runner_file', + lambda _: sample_runner_config, + ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dsr1-fp8-mi300x-sglang', + '--evals-only', + '--all-evals', + '--trim-conc', + ]) + + result = generate_sweep_configs.main() + + assert len(result) == 1 + assert result[0]['conc'] == 4 + assert result[0]['run-eval'] is True + assert result[0]['eval-only'] is True + + def test_trim_conc_updates_multinode_dispatch_concurrency(self): + low_entry = { + 'prefill': {'num-worker': 1, 'tp': 8}, + 'decode': {'num-worker': 0, 'tp': 8}, + 'conc': [4], + } + high_entry = { + **low_entry, + 'conc': [64], + 'run-eval': True, + 'eval-conc': 64, + } + + result = trim_conc([high_entry, low_entry]) + + assert len(result) == 1 + assert result[0]['conc'] == [4] + assert result[0]['eval-conc'] == 4 + assert result[0]['run-eval'] is True + + def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( + self, + monkeypatch, + ): + import sys + import generate_sweep_configs + + repo_root = Path(__file__).resolve().parents[2] + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'full-sweep', + '--config-files', + str(repo_root / 'configs/nvidia-master.yaml'), + str(repo_root / 'configs/amd-master.yaml'), + '--runner-config', + str(repo_root / 'configs/runners.yaml'), + '--model-prefix', + 'kimik3', + 'minimaxm3', + '--scenario-type', + 'agentic-coding', + '--evals-only', + '--all-evals', + '--trim-conc', + ]) + + rows = generate_sweep_configs.main() + manifest_fields = ( + 'model-prefix', + 'runner', + 'framework', + 'precision', + 'tp', + 'pp', + 'dcp-size', + 'pcp-size', + 'ep', + 'dp-attn', + 'prefill', + 'decode', + 'disagg', + 'kv-offloading', + 'kv-offload-backend', + 'spec-decoding', + 'exp-name', + ) + manifest = sorted( + tuple( + ( + field, + generate_sweep_configs.freeze_config_value(row.get(field)), + ) + for field in manifest_fields + ) + for row in rows + ) + manifest_digest = hashlib.sha256( + json.dumps(manifest, separators=(',', ':')).encode() + ).hexdigest() + + assert len(manifest) == 36 + assert manifest_digest == ( + 'c37c4259ca15d7b4856166a4703a7cc6ed43d4b86262cd6c58433c19fdcfa813' + ), json.dumps(manifest, indent=2) + for row in rows: + if isinstance(row['conc'], list): + assert row['conc'] == [row['eval-conc']] + assert all(row['run-eval'] is True for row in rows) + assert all(row['eval-only'] is True for row in rows) + def test_all_evals_batches_each_multinode_concurrency( self, monkeypatch, diff --git a/utils/process_changelog.py b/utils/process_changelog.py index 6149d8fba7..e246ce788f 100644 --- a/utils/process_changelog.py +++ b/utils/process_changelog.py @@ -12,7 +12,11 @@ import yaml from constants import GENERATE_SWEEPS_PY_SCRIPT, MASTER_CONFIGS -from matrix_logic.generate_sweep_configs import seq_len_to_str +from matrix_logic.generate_sweep_configs import ( + freeze_config_value, + seq_len_to_str, + trim_conc, +) from matrix_logic.validation import ( ChangelogEntry, ChangelogMatrixEntry, @@ -29,15 +33,6 @@ class GenerationInputs: runner_config: str -def _freeze_config_value(value): - """Convert JSON-shaped config values into deterministic hashable values.""" - if isinstance(value, dict): - return tuple( - sorted((key, _freeze_config_value(item)) for key, item in value.items()) - ) - if isinstance(value, list): - return tuple(_freeze_config_value(item) for item in value) - return value def get_added_lines(base_ref: str, head_ref: str, filepath: str) -> str: @@ -67,54 +62,6 @@ def get_added_lines(base_ref: str, head_ref: str, filepath: str) -> str: return "\n".join(added_lines) -def trim_conc(entries: list[dict]) -> list[dict]: - """Trim each parallelism config's concurrency sweep to its lowest point. - - Non-full-sweep PRs only need a single concurrency point per parallelism - config to validate a change runs end-to-end, so the shared cluster stays - clear. Push-to-main and ``full-sweep-enabled`` PRs skip this reduction. - - The retained value is the minimum configured concurrency — independent of - the source ordering of ``conc-list`` / ``conc-start``. - - Input comes from ``json.loads(subprocess.stdout)`` so ``conc`` is always - ``int`` (single-node) or ``list`` (multi-node). Other fields may contain - nested dictionaries or lists, such as KV-offload backend metadata. - - - Single-node entries: group by every configuration field other than - ``conc`` and the generated ``exp-name``, then keep only the entry with - the lowest ``conc`` per group. - - Multi-node entries: trim the ``conc`` list in place to ``[min(conc)]``. - """ - groups: dict[tuple, list[int]] = {} - out: list[dict] = [] - - for entry in entries: - if entry.get("prefill") is not None: - conc = entry.get("conc") - if isinstance(conc, list) and len(conc) > 1: - entry = {**entry, "conc": [min(conc)]} - out.append(entry) - continue - - key = tuple( - sorted( - (k, _freeze_config_value(v)) - for k, v in entry.items() - if k not in {"conc", "exp-name"} - ) - ) - groups.setdefault(key, []).append(len(out)) - out.append(entry) - - drop: set[int] = set() - for idxs in groups.values(): - if len(idxs) > 1: - keep = min(idxs, key=lambda i: out[i]["conc"]) - drop.update(i for i in idxs if i != keep) - return [e for i, e in enumerate(out) if i not in drop] - - def filter_eval_rows_by_prefill_ep( eval_rows: list[dict], min_prefill_ep: int | None ) -> list[dict]: @@ -210,7 +157,7 @@ def _matrix_curve_key(entry: dict) -> tuple: """Identify one curve while deliberately excluding point-level fields.""" return tuple( sorted( - (key, _freeze_config_value(value)) + (key, freeze_config_value(value)) for key, value in entry.items() if key not in {"conc", "exp-name", "recipe-fingerprint"} ) diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index 081a10a6ba..b34c87f8b0 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -165,7 +165,7 @@ def test_collect_eval_rows_accepts_minimax_compatibility_result( assert rows[0]["eval_suite"] == "minimax_m3_smoke" -def test_collect_eval_rows_excludes_integration_and_sample_failures( +def test_collect_eval_rows_retains_integration_and_sample_failures( tmp_path: Path, ) -> None: for name, invalid in ( @@ -192,7 +192,51 @@ def test_collect_eval_rows_excludes_integration_and_sample_failures( result["n-samples"]["gsm8k"]["effective"] = invalid result_path.write_text(json.dumps(result)) - assert collect_eval_rows(tmp_path) == [] + rows = collect_eval_rows(tmp_path) + assert len(rows) == 5 + assert all(row["infrastructure_success"] is False for row in rows) + assert all(row["score"] is None for row in rows) + assert all( + row["n_eff"] == 0 + for row in rows + if row["integration_error"]["type"] == "InvalidEffectiveSampleCount" + ) + assert { + row["integration_error"]["type"] + for row in rows + } == {"RuntimeError", "InvalidEffectiveSampleCount"} + + +def test_collect_eval_rows_handles_malformed_failure_metadata( + tmp_path: Path, +) -> None: + for index, (configs, sample_counts) in enumerate( + ( + (None, None), + ({"gsm8k": None}, {"gsm8k": None}), + ) + ): + artifact_dir = tmp_path / f"eval_malformed_metadata_{index}" + artifact_dir.mkdir() + (artifact_dir / "meta_env.json").write_text( + json.dumps({"eval_suite": "gsm8k"}) + ) + result_path = artifact_dir / f"results_{index}.json" + _write_lm_eval_result(result_path, 0.0) + result = json.loads(result_path.read_text()) + result["configs"] = configs + result["n-samples"] = sample_counts + result["integration_error"] = { + "type": "RuntimeError", + "message": "setup failed", + } + result_path.write_text(json.dumps(result)) + + rows = collect_eval_rows(tmp_path) + assert len(rows) == 2 + assert all(row["score"] is None for row in rows) + assert all(row["n_eff"] == 0 for row in rows) + assert all(row["infrastructure_success"] is False for row in rows) def test_collect_eval_rows_accepts_legacy_missing_effective_count( @@ -241,7 +285,12 @@ def test_collect_eval_rows_does_not_resurrect_stale_valid_result( current_path.touch() stale_path.touch() - assert collect_eval_rows(tmp_path) == [] + rows = collect_eval_rows(tmp_path) + assert len(rows) == 1 + assert rows[0]["infrastructure_success"] is False + assert rows[0]["integration_error"]["message"] == ( + "vendor verifier checkout failed" + ) def test_collect_eval_rows_uses_mtime_for_newer_legacy_name( @@ -266,10 +315,13 @@ def test_collect_eval_rows_uses_mtime_for_newer_legacy_name( current_path.write_text(json.dumps(current)) os.utime(current_path, (2_000_000_000, 2_000_000_000)) - assert collect_eval_rows(tmp_path) == [] + rows = collect_eval_rows(tmp_path) + assert len(rows) == 1 + assert rows[0]["infrastructure_success"] is False + assert rows[0]["integration_error"]["message"] == "latest attempt failed" -def test_collect_eval_rows_rejects_missing_or_out_of_range_scores( +def test_collect_eval_rows_retains_missing_or_out_of_range_scores( tmp_path: Path, ) -> None: for index, score in enumerate( @@ -286,7 +338,13 @@ def test_collect_eval_rows_rejects_missing_or_out_of_range_scores( task="kimi_tool_call_schema", ) - assert collect_eval_rows(tmp_path) == [] + rows = collect_eval_rows(tmp_path) + assert len(rows) == 6 + assert all(row["score"] is None for row in rows) + assert all(row["infrastructure_success"] is False for row in rows) + assert { + row["integration_error"]["type"] for row in rows + } == {"InvalidPrimaryScore"} def test_collect_eval_rows_falls_back_for_invalid_filename_timestamp( From b8d681f537b6366368cb24890f4cb8a1eec93525 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:41:57 -0500 Subject: [PATCH 59/99] fix: normalize infrastructure failure sample counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将所有基础设施失败的有效样本数统一归零,避免聚合结果误报已完成样本。 --- utils/collect_eval_results.py | 2 +- utils/test_collect_eval_results.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 0f86715632..567f877260 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -223,7 +223,7 @@ def extract_lm_metrics(json_path: Path) -> List[Dict[str, Any]]: 'flex_se': None, 'accuracy': None, 'accuracy_se': None, - 'n_eff': 0 if invalid_effective_count else n_eff, + 'n_eff': 0, 'model': model, 'source': str(json_path), 'infrastructure_success': False, diff --git a/utils/test_collect_eval_results.py b/utils/test_collect_eval_results.py index b34c87f8b0..6faad14111 100644 --- a/utils/test_collect_eval_results.py +++ b/utils/test_collect_eval_results.py @@ -196,11 +196,7 @@ def test_collect_eval_rows_retains_integration_and_sample_failures( assert len(rows) == 5 assert all(row["infrastructure_success"] is False for row in rows) assert all(row["score"] is None for row in rows) - assert all( - row["n_eff"] == 0 - for row in rows - if row["integration_error"]["type"] == "InvalidEffectiveSampleCount" - ) + assert all(row["n_eff"] == 0 for row in rows) assert { row["integration_error"]["type"] for row in rows From b816503ae3e07515f5dda685f72947b093d94f5e Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:56:17 -0500 Subject: [PATCH 60/99] ci: repair amd workspace ownership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:修复 AMD 运行器工作区权限,确保中断后的基准测试产物不会阻塞后续检出。 --- .github/workflows/benchmark-tmpl.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 1b3fc3fbd5..1a86ffd7e8 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -254,6 +254,13 @@ jobs: steps: - name: Resource cleanup (pre-run) run: &resource-cleanup | + # Containerized AMD Slurm jobs can leave root-owned artifacts when + # interrupted before their launcher EXIT trap runs. Repair ownership + # before checkout cleanup and again after the job via this shared step. + if [[ "${{ inputs.runner }}" == "cluster:mi355x-amds" && -d "$GITHUB_WORKSPACE" ]]; then + sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" + fi + # Cleanup Docker resources if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then echo "[Docker] Cleaning up resources ..." From 6cc9937dda9d05526543f7616821277d218761a9 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:06:50 -0500 Subject: [PATCH 61/99] fix: preserve kimi quality failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将 Kimi 模型质量失败交由阈值判定,并避免把 pytest 源码上下文误判为端点故障。 --- utils/evals/kimi_vendor_eval.py | 6 ++++-- utils/evals/test_kimi_vendor_eval.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 9d95462a19..09adf66f6b 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -27,7 +27,9 @@ RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "kimi-vendor-verifier" -ENDPOINT_REJECTION_RE = re.compile(r"(?im)tool schema rejected:") +ENDPOINT_REJECTION_RE = re.compile( + r"(?im)^(?:E\s+)?AssertionError:.*tool schema rejected:" +) def prepare_compatibility_path(output_dir: Path) -> Path: @@ -398,7 +400,7 @@ def run_evaluation( integration_error=integration_error, ) completed_successfully = False - elif task_name == FULL_TASK_NAME and valid_outcome: + elif valid_outcome: completed_successfully = True elif not valid_outcome: integration_error = RuntimeError( diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index d8cb08707b..b8c63611de 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -201,7 +201,7 @@ def test_builds_dsv4_thinking_command(tmp_path: Path) -> None: ("passed", 0, True, 1.0, 2, None), ("passed", 1, False, 0.0, 0, "RuntimeError"), ("failed", 0, False, 0.0, 0, "RuntimeError"), - ("failed", 1, False, 0.5, 2, None), + ("failed", 1, True, 0.5, 2, None), ("failed", 2, False, 0.0, 0, "RuntimeError"), ), ) @@ -260,7 +260,8 @@ def test_full_report_projects_all_mode_records_and_defers_quality_gating( output_dir = tmp_path / "output" report = _full_report(failed_records=1) report["results"][0]["message"] = ( - "AssertionError: TestSchema:1 [non-stream] (all) " + ' f"tool schema rejected: {response.message}"\n' + "E AssertionError: TestSchema:1 [non-stream] (all) " "arguments validation failed: 'bad' is not valid" ) native_bytes = json.dumps(report).encode() From 240a56421508720887386ec4cbc509037d8a09e0 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:00:56 -0500 Subject: [PATCH 62/99] fix: harden bfcl transport retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为 BFCL 请求增加有界传输重试,避免单次超时使完整评估失效,并在原生报告中记录重试策略。 --- utils/evals/EVALS.md | 13 +++++----- utils/evals/bfcl_adapter.py | 46 ++++++++++++++++++++++++++++++++--- utils/evals/test_bfcl_eval.py | 16 ++++++++++-- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 287660b2fe..7220d713e7 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -333,8 +333,9 @@ typically `http://127.0.0.1:$PORT/v1`. The OpenAI SDK appends not download a model or call a remote inference API. The smoke fixes temperature to `0`, uses four BFCL worker threads, allows 180 -seconds per OpenAI request with retries disabled, and has a 900-second -whole-suite timeout. Dependency installation is separately bounded at 600 +seconds per OpenAI request, and permits two bounded OpenAI client retries for +retryable transport and server failures (three total attempts). The whole-suite +timeout is 900 seconds. Dependency installation is separately bounded at 600 seconds. Dependency, setup, transport, timeout, and collection failures write zero-score artifacts with integration-error metadata and fail the runner nonzero. A completed evaluation exits independently of model quality; the @@ -395,10 +396,10 @@ the V4 agentic web-search and memory evaluations. Select these suites explicitly with `eval-framework: bfcl`; `bfcl_smoke` remains the framework default. Both suites use BFCL's OpenAI completions handler against the local endpoint rather than a hosted-provider handler. They -fix temperature to `0.001`, disable request retries, and keep the 180-second -per-request timeout. MiniMax uses eight worker threads. Kimi uses 16 threads -and permits up to ten multi-turn steps. The whole-suite timeout is 7200 -seconds. +fix temperature to `0.001`, permit the same two bounded request retries, and +keep the 180-second per-attempt timeout. MiniMax uses eight worker threads. +Kimi uses 16 threads and permits up to ten multi-turn steps. The whole-suite +timeout is 7200 seconds. The adapter builds a deterministic run-ID map from the pinned BFCL dataset. Single-turn suites select every case in their named categories. The Kimi diff --git a/utils/evals/bfcl_adapter.py b/utils/evals/bfcl_adapter.py index 72af4cb588..8c0b2d4780 100644 --- a/utils/evals/bfcl_adapter.py +++ b/utils/evals/bfcl_adapter.py @@ -26,6 +26,7 @@ ADAPTER_NAME = "bfcl-v4-openai-completions" DEFAULT_NUM_THREADS = 4 DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 +DEFAULT_REQUEST_MAX_RETRIES = 2 REQUIRED_SCORE = 0.75 BFCL_PACKAGE = "bfcl-eval" @@ -158,6 +159,7 @@ def __call__( api_key: str, num_threads: int, request_timeout_seconds: float, + request_max_retries: int, ) -> None: ... @@ -226,6 +228,16 @@ def _positive_int(value: str) -> int: return parsed +def _nonnegative_int(value: str) -> int: + try: + parsed = int(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("must be a non-negative integer") from exc + if parsed < 0: + raise argparse.ArgumentTypeError("must be a non-negative integer") + return parsed + + def _positive_float(value: str) -> float: try: parsed = float(value) @@ -299,6 +311,8 @@ def _native_report( model: str, base_url: str | None, num_threads: int, + request_timeout_seconds: float, + request_max_retries: int, scores: Sequence[CategoryScore] | None, integration_error: BaseException | None = None, ) -> dict[str, Any]: @@ -316,6 +330,8 @@ def _native_report( "sampling": { "temperature": suite.temperature, "num_threads": num_threads, + "request_timeout_seconds": request_timeout_seconds, + "request_max_retries": request_max_retries, }, "summary": { "accuracy": accuracy, @@ -606,6 +622,7 @@ def _run_upstream( api_key: str, num_threads: int, request_timeout_seconds: float, + request_max_retries: int, ) -> None: """Lazily load and invoke the pinned BFCL API against an existing server.""" suite, case_ids_by_category = _read_selected_suite(project_root) @@ -628,12 +645,15 @@ def _run_upstream( class BoundedOpenAICompletionsHandler(OpenAICompletionsHandler): def _build_client_kwargs(self) -> dict[str, Any]: kwargs = super()._build_client_kwargs() - kwargs.update(timeout=request_timeout_seconds, max_retries=0) + kwargs.update( + timeout=request_timeout_seconds, + max_retries=request_max_retries, + ) return kwargs def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: - # The upstream method has an unbounded RateLimitError retry decorator. - # The surrounding eval process owns the suite deadline, so issue once. + # Replace upstream's unbounded RateLimitError decorator with the + # OpenAI client's bounded transport and server-error retries. started = time.monotonic() try: response = self.client.chat.completions.create(**kwargs) @@ -870,6 +890,8 @@ def publish_integration_error( model=model, base_url=None, num_threads=suite.default_num_threads, + request_timeout_seconds=DEFAULT_REQUEST_TIMEOUT_SECONDS, + request_max_retries=DEFAULT_REQUEST_MAX_RETRIES, scores=None, integration_error=error, ), @@ -896,6 +918,7 @@ def run_evaluation( suite: SuiteSpec = SMOKE_SUITE, num_threads: int | None = None, request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, + request_max_retries: int = DEFAULT_REQUEST_MAX_RETRIES, upstream_runner: UpstreamRunner = _run_upstream, ) -> bool: """Run one immutable BFCL suite and always publish both report formats.""" @@ -927,6 +950,12 @@ def run_evaluation( or request_timeout_seconds <= 0 ): raise ValueError("request_timeout_seconds must be positive and finite") + if ( + isinstance(request_max_retries, bool) + or not isinstance(request_max_retries, int) + or request_max_retries < 0 + ): + raise ValueError("request_max_retries must be a non-negative integer") if not callable(upstream_runner): raise TypeError("upstream_runner must be callable") @@ -942,6 +971,7 @@ def run_evaluation( api_key=normalized_key, num_threads=resolved_num_threads, request_timeout_seconds=float(request_timeout_seconds), + request_max_retries=request_max_retries, ) _validate_generated_results(bfcl_project_root, selected_case_ids) scores = _collect_scores(bfcl_project_root, selected_case_ids) @@ -958,6 +988,8 @@ def run_evaluation( model=model, base_url=base_url, num_threads=resolved_num_threads, + request_timeout_seconds=float(request_timeout_seconds), + request_max_retries=request_max_retries, scores=None, integration_error=exc, ), @@ -980,6 +1012,8 @@ def run_evaluation( model=normalized_model, base_url=normalized_url, num_threads=resolved_num_threads, + request_timeout_seconds=float(request_timeout_seconds), + request_max_retries=request_max_retries, scores=scores, ) compatibility = _compatibility_result( @@ -1013,6 +1047,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: type=_positive_float, default=DEFAULT_REQUEST_TIMEOUT_SECONDS, ) + parser.add_argument( + "--request-max-retries", + type=_nonnegative_int, + default=DEFAULT_REQUEST_MAX_RETRIES, + ) parser.add_argument("--integration-error") args = parser.parse_args(argv) args.num_threads = ( @@ -1051,6 +1090,7 @@ def main(argv: Sequence[str] | None = None) -> int: suite=suite, num_threads=args.num_threads, request_timeout_seconds=args.request_timeout_seconds, + request_max_retries=args.request_max_retries, ) return 0 if passed else 1 diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index 41092812a2..e64b64ca83 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -220,6 +220,7 @@ def test_command_defaults_and_required_runtime_inputs(tmp_path: Path) -> None: assert args.api_key == "EMPTY" assert args.num_threads == 4 assert args.request_timeout_seconds == 180.0 + assert args.request_max_retries == 2 assert args.integration_error is None assert args.suite == be.TASK_NAME with pytest.raises(SystemExit): @@ -291,12 +292,17 @@ def run_evaluation(**kwargs: Any) -> bool: assert return_code == 0 assert invocation["suite"] is be.KIMI_SUITE assert invocation["num_threads"] == 16 + assert invocation["request_max_retries"] == 2 @pytest.mark.parametrize( ("flag", "value"), - (("--num-threads", "0"), ("--request-timeout-seconds", "nan")), + ( + ("--num-threads", "0"), + ("--request-timeout-seconds", "nan"), + ("--request-max-retries", "-1"), + ), ) def test_cli_rejects_invalid_positive_values( tmp_path: Path, flag: str, value: str @@ -358,6 +364,7 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( "api_key": "EMPTY", "num_threads": 4, "request_timeout_seconds": 180.0, + "request_max_retries": 2, } assert json.loads( (project_root / "test_case_ids_to_generate.json").read_text(encoding="utf-8") @@ -900,7 +907,12 @@ def test_selected_suite_integration_error_preserves_suite_identity( native = _native(output_dir) assert native["task"] == "bfcl_vllm_minimax_m3" assert native["summary"]["expected_count"] == 1000 - assert native["sampling"] == {"temperature": 0.001, "num_threads": 8} + assert native["sampling"] == { + "temperature": 0.001, + "num_threads": 8, + "request_timeout_seconds": 180.0, + "request_max_retries": 2, + } assert list(compatibility["results"]) == [ "bfcl_vllm_minimax_m3", "bfcl_vllm_minimax_m3_simple_python", From 708c8b551d3164cdcdcf980a65b8f4cf62eaba18 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:15:45 -0500 Subject: [PATCH 63/99] fix: preserve stock tool use validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保持工具调用验证器原样运行 --- benchmarks/benchmark_lib.sh | 73 +- utils/evals/EVALS.md | 107 +-- utils/evals/bfcl_adapter.py | 130 +-- utils/evals/kimi_vendor_eval.py | 22 +- utils/evals/minimax_provider_eval.py | 988 +++++----------------- utils/evals/test_bfcl_eval.py | 153 ++-- utils/evals/test_kimi_vendor_eval.py | 37 +- utils/evals/test_minimax_provider_eval.py | 863 ++++--------------- utils/evals/test_run_eval_dispatch.py | 449 +++++----- 9 files changed, 840 insertions(+), 1982 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 3bc6c753d6..bf3b6bc641 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -968,7 +968,6 @@ _install_kimi_vendor_eval_deps() { "openai==2.14.0" "jsonschema==4.25.1" "pytest==8.4.2" - "pytest-rerunfailures==16.4" ) if [ "$eval_suite" = "kimi_tool_call_schema_full" ]; then packages+=("pytest-xdist==3.8.0") @@ -992,6 +991,7 @@ _prepare_kimi_vendor_runtime() { _prepare_kimi_vendor_verifier() { local repo_url="$1" local verifier_ref="$2" + local expected_archive_sha256="$3" local checkout_dir prepare_rc=0 checkout_dir="$(mktemp -d /tmp/kimi-vendor-verifier-XXXXXX)" || { @@ -999,7 +999,9 @@ _prepare_kimi_vendor_verifier() { return 1 } - "${VENDOR_VERIFIER_PYTHON:-python3}" - "$repo_url" "$verifier_ref" "$checkout_dir" <<'PY' || prepare_rc=$? + "${VENDOR_VERIFIER_PYTHON:-python3}" - \ + "$repo_url" "$verifier_ref" "$expected_archive_sha256" "$checkout_dir" <<'PY' || prepare_rc=$? +from hashlib import sha256 from pathlib import Path import re import socket @@ -1012,7 +1014,7 @@ from urllib.parse import quote, urlsplit, urlunsplit from urllib.request import Request, urlopen -repo_url, verifier_ref, checkout_dir_arg = sys.argv[1:] +repo_url, verifier_ref, expected_archive_sha256, checkout_dir_arg = sys.argv[1:] checkout_dir = Path(checkout_dir_arg) stage = "derive the pinned archive URL" @@ -1030,6 +1032,11 @@ def archive_member_parts(name): try: if not re.fullmatch(r"[0-9a-fA-F]{40}", verifier_ref): raise ValueError(f"expected a 40-character commit SHA, got {verifier_ref!r}") + if not re.fullmatch(r"[0-9a-fA-F]{64}", expected_archive_sha256): + raise ValueError( + "expected a 64-character archive SHA256, got " + f"{expected_archive_sha256!r}" + ) parsed_repo_url = urlsplit(repo_url) if parsed_repo_url.scheme not in ("http", "https") or not parsed_repo_url.netloc: @@ -1057,6 +1064,7 @@ try: archive_file.seek(0) archive_file.truncate() downloaded = 0 + digest = sha256() deadline = time.monotonic() + 60 try: with urlopen(request, timeout=60) as response: @@ -1084,6 +1092,7 @@ try: "archive download exceeds the 128 MiB safety limit" ) archive_file.write(chunk) + digest.update(chunk) break except HTTPError as error: if error.code not in (408, 429) and not 500 <= error.code < 600: @@ -1107,6 +1116,12 @@ try: time.sleep(attempt) if downloaded == 0: raise ValueError("downloaded archive is empty") + actual_archive_sha256 = digest.hexdigest() + if actual_archive_sha256 != expected_archive_sha256: + raise ValueError( + "Kimi-Vendor-Verifier archive SHA256 mismatch: expected " + f"{expected_archive_sha256}, got {actual_archive_sha256}" + ) archive_file.seek(0) stage = "validate the downloaded archive" @@ -1309,6 +1324,7 @@ _run_kimi_tool_call_schema_eval() { local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}" local verifier_repo="https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" local verifier_ref="b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + local verifier_archive_sha256="ab933117c894a785978f8aee0f052e5a9096b3029e7962354b1c07ea430588c3" local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" local timeout_seconds=900 if [ "$eval_suite" = "kimi_tool_call_schema_full" ]; then @@ -1358,7 +1374,8 @@ _run_kimi_tool_call_schema_eval() { fi if [ "$setup_rc" -eq 0 ]; then checkout_dir=$( - _prepare_kimi_vendor_verifier "$verifier_repo" "$verifier_ref" + _prepare_kimi_vendor_verifier \ + "$verifier_repo" "$verifier_ref" "$verifier_archive_sha256" ) || { setup_rc=$? integration_error="Kimi Vendor Verifier checkout failed with exit code ${setup_rc}" @@ -1645,7 +1662,6 @@ _run_bfcl_suite_eval() { --bfcl-project-root "$project_root" \ "${suite_args[@]}" \ --num-threads "$num_threads" \ - --request-timeout-seconds 180 \ || eval_rc=$? local archive_rc=0 if [ "$archive_upstream" = true ]; then @@ -1703,22 +1719,6 @@ run_bfcl_eval() { esac } -_install_minimax_vendor_eval_deps() { - local target_dir="$1" - "${VENDOR_VERIFIER_PYTHON:-python3}" -m pip install -q --no-cache-dir --target "$target_dir" \ - "jsonschema==4.25.1" -} - -_prepare_minimax_vendor_runtime() { - local runtime_dir install_rc=0 - runtime_dir="$(mktemp -d /tmp/minimax-vendor-runtime-XXXXXX)" || return $? - _install_minimax_vendor_eval_deps "$runtime_dir" >&2 || install_rc=$? - if [ "$install_rc" -ne 0 ]; then - rm -rf "$runtime_dir" - return "$install_rc" - fi - printf '%s\n' "$runtime_dir" -} _write_minimax_vendor_integration_error() { local adapter_path="$1" @@ -1726,12 +1726,12 @@ _write_minimax_vendor_integration_error() { local results_dir="$3" local message="$4" - # The adapter's integration-error path is stdlib-only, so it remains usable - # when Python provisioning or dependency installation is what failed. - "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ + # The failure path is stdlib-only, so it remains usable when runtime + # provisioning or dependency installation is what failed. + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" failure \ --model "$model_name" \ --output-dir "$results_dir" \ - --integration-error "$message" + --message "$message" } _run_minimax_m3_smoke_eval() { @@ -1777,9 +1777,9 @@ _run_minimax_m3_smoke_eval() { integration_error="MiniMax Provider Verifier Python runtime preparation failed with exit code ${setup_rc}" } if [ "$setup_rc" -eq 0 ]; then - runtime_dir=$(_prepare_minimax_vendor_runtime) || { + runtime_dir=$(_prepare_minimax_m3_full_runtime "$adapter_path") || { setup_rc=$? - integration_error="MiniMax Provider Verifier dependency installation failed with exit code ${setup_rc}" + integration_error="MiniMax Provider Verifier pinned runtime preparation failed with exit code ${setup_rc}" } fi if [ "$setup_rc" -ne 0 ]; then @@ -1797,16 +1797,15 @@ _run_minimax_m3_smoke_eval() { fi local eval_rc=0 - PYTHONPATH="${runtime_dir}${PYTHONPATH:+:${PYTHONPATH}}" \ - "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" \ - --base-url "http://127.0.0.1:${port}/v1" \ - --api-key EMPTY \ - --model "$model_name" \ - --output-dir "$results_dir" \ - --fixture "$fixture_path" \ - --request-timeout-seconds 180 \ - --timeout-seconds 900 \ - || eval_rc=$? + "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" run \ + --python "${VENDOR_VERIFIER_PYTHON:-python3}" \ + --source-dir "${runtime_dir}/source" \ + --dependency-dir "${runtime_dir}/deps" \ + --base-url "http://127.0.0.1:${port}/v1" \ + --model "$model_name" \ + --output-dir "$results_dir" \ + --fixture "$fixture_path" \ + || eval_rc=$? if [ "$eval_rc" -ne 0 ] \ && ! _has_eval_result "$results_dir" "results_minimax_vendor_"; then integration_error="MiniMax Provider Verifier failed with exit code ${eval_rc}" diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 7220d713e7..0b22584b69 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -123,42 +123,37 @@ launch their existing `*_mtp.sh` server instead of silently falling back to STP. The smoke runs the unmodified [MoonshotAI/Kimi-Vendor-Verifier](https://github.com/MoonshotAI/Kimi-Vendor-Verifier) -at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run downloads the -fresh pinned GitHub source archive and safely extracts only the upstream pytest -configuration, tool-call schema tests, and bundled Walle cases. InferenceX does -not install the verifier package or reimplement its request, streaming, or -validation logic. +at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run downloads and +SHA256-verifies the fresh pinned GitHub source archive, then safely extracts +only the upstream pytest configuration, tool-call schema tests, and bundled +Walle cases. InferenceX does not install the verifier package or reimplement +its request, streaming, or validation logic. System Python 3.12 or newer is preferred and used directly. On older images, the runner uses the existing system `pip` to install pinned `uv==0.11.33` under a temporary prefix, then provisions an isolated Python 3.12 virtual environment. The selected interpreter installs the minimal pinned verifier runtime -(`httpx[http2]`, `openai`, `jsonschema`, `pytest`, and -`pytest-rerunfailures`) into a separate temporary package directory, then runs -upstream `tests/tool_call_json_schema/test_tool_call_json_schema.py` with: +(`httpx[http2]`, `openai`, `jsonschema`, and `pytest`) into a separate +temporary package directory, then runs upstream +`tests/tool_call_json_schema/test_tool_call_json_schema.py` with: - the local OpenAI-compatible endpoint, `EMPTY` API key, and served model name; - `--think-mode none` for other models, or `--think-mode opensource --thinking` for `dsv4`, plus `--selection object --max-cases 1 --max-tokens 2048`; -- up to six three-second reruns, limited to transient HTTP and transport errors; - the bundled Walle case directory and `--tool-json-report`. The temporary Python runtime, package directory, and verifier checkout are removed after both successful and failed runs. The selection is `TestAdditionalProperties:1`, parametrized upstream in -non-streaming and streaming modes. Pytest makes one initial attempt and up to -six reruns of each mode at three-second intervals, but only for HTTP 404, 429, -5xx, connection, and timeout failures. This covers frontends whose health route -becomes ready shortly before chat completions without retrying schema or -model-output failures. The unchanged native report remains one final -outcome per mode because the upstream report deduplicates rerun records by case -and mode. It is uploaded as `kimi_vendor_report.json`, and +non-streaming and streaming modes. Each mode runs once through the unchanged +upstream pytest harness. The unchanged native report remains one final outcome +per mode. It is uploaded as `kimi_vendor_report.json`, and `utils/evals/kimi_vendor_eval.py` projects those two outcomes into the existing eval result shape. Both must pass, so the `kimi_tool_call_schema` threshold is `1.0`. Setup, timeout, and collection failures emit a zero-score result with error metadata. The adapter's 900-second global timeout bounds the entire -upstream pytest process, including all attempts and rerun delays. +upstream pytest process. This smoke validates one object-schema tool call. It does not cover tool choice, parallel calls, multi-turn execution, or general agent quality. Multi-value @@ -180,8 +175,8 @@ Select it explicitly with `eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema_full`. Its threshold is `0.0`, so model quality is diagnostic while setup, timeout, malformed-report, and integration failures still fail through the standard zero-effective-sample error path. The -full suite reuses the smoke's pinned checkout, retry policy, result envelope, -artifact staging, collector, and dashboard path. +full suite reuses the smoke's pinned checkout, stock invocation, result +envelope, artifact staging, collector, and dashboard path. ### MiniMax provider compatibility smoke @@ -208,41 +203,31 @@ python3 utils/evals/validate_scores.py `85bf180e54e2ab0b31595cfdc697116c4760876d`. The vendored fixture retains the full upstream MIT copyright, permission, and warranty notice. It contains only upstream zero-based row 71, an `expected_tool_call: true` request -exercising tool-call trigger and argument-schema validation. The adapter -applies the pinned validator semantics directly to this fixture; it does not -download the upstream repository or run the remaining 101 cases. - -The adapter sends the request to `${base_url}/chat/completions`. The endpoint -must accept an OpenAI-compatible Bearer token and chat-completions request body -and return OpenAI-compatible message, finish-reason, and tool-call fields. -Redirect responses are rejected before forwarding bearer credentials; the -local runner supplies `Authorization: Bearer EMPTY`. The smoke uses -`temperature: 0`, `top_p: 1`, and `max_tokens: 40960`. The token budget matches -the pinned verifier's MiniMax M3 default and prevents a valid tool-call response -from ending at the model's common 2048-token generation default. The request -has a 180-second timeout by default and at most three retries for transport -failures, HTTP 429, or HTTP 5xx responses (four total attempts); a hard -900-second global bound covers the smoke. - -`minimax_vendor_report.json` is the native report. It preserves the raw -response and reports the six upstream-derived metric fields. This Phase 1 case -exercises `Query-Success-Rate`, `ToolCalls-Trigger-Similarity`, -`ToolCalls-Schema-Accuracy`, and `Error-Only-Reasoning-Rate`. -`Language-Following-Success-Rate` and `Scenario-Check-Pass-Rate` have no -applicable case in this fixture and report `0.0` with zero checked counts. - -The adapter additionally writes exactly one timestamped -`results_minimax_vendor_*.json` compatibility artifact. Its `result_format` is -`inferencex-eval-v1`, `eval_adapter` is `minimax-provider-verifier`, task is -`minimax_m3_smoke`, and primary metric is `exact_match,strict-match`. A -completed run records original and effective sample counts of one. Its score -is `1.0` only when row 71 returns a `tool_calls` finish reason and every -function call validates against the requested schema. The -`minimax_m3_smoke` threshold remains `1.0`. -Both artifacts match the workflows' existing `results*.json` and -`*_vendor_report.json` upload patterns. -Setup, integration, timeout, and collection failures still emit a zero-score -compatibility artifact with error metadata. +exercising tool-call trigger and argument-schema validation. + +Each run downloads and hash-verifies the pinned upstream `verify.py`, complete +`sample.jsonl`, and validator modules. InferenceX writes row 71 unchanged to a +temporary JSONL input and invokes the stock verifier with its documented CLI. +The invocation uses concurrency one, the stock 600-second request timeout and +three-retry setting, and the documented `--extra-body` override +`{\"temperature\":0,\"top_p\":1,\"max_tokens\":40960}`. A one-hour outer +process deadline bounds the stock harness without changing its request, +response, retry, or scoring code. + +`minimax_vendor_report.json` and `minimax_vendor_results.jsonl` are the +unchanged stock summary and detailed result artifacts. The adapter additionally +writes exactly one timestamped `results_minimax_vendor_*.json` compatibility +artifact. Its `result_format` is `inferencex-eval-v1`, `eval_adapter` is +`minimax-provider-verifier`, task is `minimax_m3_smoke`, and primary metric is +`exact_match,strict-match`. A completed run records original and effective +sample counts of one. Its score is the minimum of the stock verifier's +tool-call match rate, tool-call schema accuracy, and one minus its +error-only-reasoning rate. The `minimax_m3_smoke` threshold remains `1.0`. + +Setup, transport, timeout, malformed native output, and collection failures +emit a zero-effective-sample compatibility artifact with integration-error +metadata. A complete stock result below the threshold remains a model-quality +outcome rather than an integration failure. This is a fixed single-case provider compatibility smoke, not the full 102-case MiniMax Provider Verifier, BFCL, or a cross-model quality comparison. @@ -332,10 +317,12 @@ typically `http://127.0.0.1:$PORT/v1`. The OpenAI SDK appends `/chat/completions`; the adapter base URL is not the full endpoint. BFCL does not download a model or call a remote inference API. -The smoke fixes temperature to `0`, uses four BFCL worker threads, allows 180 -seconds per OpenAI request, and permits two bounded OpenAI client retries for -retryable transport and server failures (three total attempts). The whole-suite -timeout is 900 seconds. Dependency installation is separately bounded at 600 +The smoke fixes temperature to `0` and uses four BFCL worker threads. Request +construction, response interpretation, and retry behavior remain those of the +pinned stock BFCL OpenAI-completions handler and OpenAI SDK. The adapter only +registers the served model against that stock handler. A 900-second external +process deadline bounds the smoke; the full suites use their declared +two-hour deadline. Dependency installation is separately bounded at 600 seconds. Dependency, setup, transport, timeout, and collection failures write zero-score artifacts with integration-error metadata and fail the runner nonzero. A completed evaluation exits independently of model quality; the @@ -461,10 +448,10 @@ Key eval functions in `benchmarks/benchmark_lib.sh`: | `_install_lm_eval_deps` | Installs lm-eval dependencies | | `_prepare_vendor_verifier_python` | Uses system Python 3.12+ or provisions an isolated pinned Python 3.12 runtime for provider verifiers | | `_prepare_kimi_vendor_runtime` | Installs the pinned verifier dependencies in an isolated temp path | -| `_prepare_minimax_vendor_runtime` | Installs the pinned MiniMax adapter dependency in an isolated temp path | +| `_prepare_minimax_m3_full_runtime` | Downloads hash-verified stock MiniMax sources and installs their pinned dependencies for smoke and full suites | | `_prepare_bfcl_runtime` | Installs the verified BFCL wheel in a temporary virtual environment | | `_install_bfcl_eval_deps` | Downloads, verifies, and installs the pinned BFCL wheel | -| `_prepare_kimi_vendor_verifier` | Downloads and safely extracts a fresh subset of the pinned source archive | +| `_prepare_kimi_vendor_verifier` | Downloads, hash-verifies, and safely extracts a fresh subset of the pinned source archive | | `_patch_lm_eval` | Patches lm-eval for reasoning tokens and TRT compatibility | | `compute_eval_context_length` | Computes eval context length (requested benchmark context, capped at model native max) | | `get_native_max_context_length` | Extracts model's native max context length from HF config | diff --git a/utils/evals/bfcl_adapter.py b/utils/evals/bfcl_adapter.py index 8c0b2d4780..35737e6ac0 100644 --- a/utils/evals/bfcl_adapter.py +++ b/utils/evals/bfcl_adapter.py @@ -9,12 +9,10 @@ import math import os import sys -import time import urllib.parse from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass from pathlib import Path -from queue import SimpleQueue from types import MappingProxyType from typing import Any, Protocol @@ -25,8 +23,6 @@ RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "bfcl-v4-openai-completions" DEFAULT_NUM_THREADS = 4 -DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 -DEFAULT_REQUEST_MAX_RETRIES = 2 REQUIRED_SCORE = 0.75 BFCL_PACKAGE = "bfcl-eval" @@ -38,9 +34,7 @@ SOURCE_REVISION = "6ea57973c7a6097fd7c5915698c54c17c5b1b6c8" VLLM_INTEGRATION_REF = "7ecb11405df86b202f4c5cca322bd133052fee82" UPSTREAM_LICENSE = "Apache-2.0" -UPSTREAM_LICENSE_URL = ( - f"{UPSTREAM_REPOSITORY}/blob/{SOURCE_REVISION}/LICENSE" -) +UPSTREAM_LICENSE_URL = f"{UPSTREAM_REPOSITORY}/blob/{SOURCE_REVISION}/LICENSE" UPSTREAM_LICENSE_FILENAME = "BFCL_LICENSE.apache-2.0.txt" UPSTREAM_ATTRIBUTION_FILENAME = "BFCL_ATTRIBUTION.json" @@ -65,7 +59,6 @@ class SuiteSpec: temperature: float default_num_threads: int threshold: float - maximum_step_limit: int | None = None category_limits: tuple[tuple[str, int], ...] = () @property @@ -132,7 +125,6 @@ def projected_task(self, category: str) -> str: temperature=0.001, default_num_threads=16, threshold=0.0, - maximum_step_limit=10, category_limits=(("multi_turn", 240),), ) SUITE_SPECS: Mapping[str, SuiteSpec] = MappingProxyType( @@ -158,8 +150,6 @@ def __call__( base_url: str, api_key: str, num_threads: int, - request_timeout_seconds: float, - request_max_retries: int, ) -> None: ... @@ -228,26 +218,6 @@ def _positive_int(value: str) -> int: return parsed -def _nonnegative_int(value: str) -> int: - try: - parsed = int(value) - except ValueError as exc: - raise argparse.ArgumentTypeError("must be a non-negative integer") from exc - if parsed < 0: - raise argparse.ArgumentTypeError("must be a non-negative integer") - return parsed - - -def _positive_float(value: str) -> float: - try: - parsed = float(value) - except ValueError as exc: - raise argparse.ArgumentTypeError("must be a positive finite number") from exc - if not math.isfinite(parsed) or parsed <= 0: - raise argparse.ArgumentTypeError("must be a positive finite number") - return parsed - - def _source_details( suite: SuiteSpec, case_ids_by_category: Mapping[str, tuple[str, ...]] ) -> dict[str, Any]: @@ -311,8 +281,6 @@ def _native_report( model: str, base_url: str | None, num_threads: int, - request_timeout_seconds: float, - request_max_retries: int, scores: Sequence[CategoryScore] | None, integration_error: BaseException | None = None, ) -> dict[str, Any]: @@ -330,8 +298,6 @@ def _native_report( "sampling": { "temperature": suite.temperature, "num_threads": num_threads, - "request_timeout_seconds": request_timeout_seconds, - "request_max_retries": request_max_retries, }, "summary": { "accuracy": accuracy, @@ -473,6 +439,7 @@ def _prepare_output_paths(output_dir: Path) -> tuple[Path, Path]: compatibility_path = output_dir / COMPATIBILITY_FILENAME return native_path, compatibility_path + def _clear_upstream_modules() -> None: """Reload BFCL's import-time paths and limits for each adapter invocation.""" for module_name in tuple(sys.modules): @@ -508,15 +475,12 @@ def _function_defaults(function: Callable[..., Any]) -> dict[str, Any]: return defaults -def _load_dataset_helpers( - maximum_step_limit: int | None, -) -> tuple[Callable[[str], Any], Callable[[list[str]], Any], Callable[[Any], Any]]: +def _load_dataset_helpers() -> tuple[ + Callable[[str], Any], + Callable[[list[str]], Any], + Callable[[Any], Any], +]: """Import BFCL's pinned dataset helpers only when a full suite is selected.""" - if maximum_step_limit is not None: - import bfcl_eval.constants.default_prompts as bfcl_prompts - - # This must happen before importing utils/base_handler for multi-turn. - bfcl_prompts.MAXIMUM_STEP_LIMIT = maximum_step_limit from bfcl_eval.utils import ( load_dataset_entry, parse_test_category_argument, @@ -532,9 +496,7 @@ def _build_suite_case_ids( if suite is SMOKE_SUITE: return dict(SMOKE_CASE_IDS) - load_dataset_entry, parse_test_category_argument, sort_key = ( - _load_dataset_helpers(suite.maximum_step_limit) - ) + load_dataset_entry, parse_test_category_argument, sort_key = _load_dataset_helpers() category_limits = dict(suite.category_limits) selected_by_leaf: dict[str, tuple[str, ...]] = {} seen_ids: set[str] = set() @@ -549,9 +511,7 @@ def _build_suite_case_ids( quotas = [len(by_leaf[leaf]) for leaf in leaf_categories] else: base, extra = divmod(limit, len(leaf_categories)) - quotas = [ - base + (index < extra) for index in range(len(leaf_categories)) - ] + quotas = [base + (index < extra) for index in range(len(leaf_categories))] for leaf, quota in zip(leaf_categories, quotas, strict=True): entries = by_leaf[leaf][:quota] ids: list[str] = [] @@ -570,8 +530,7 @@ def _build_suite_case_ids( selected_by_leaf[leaf] = tuple(ids) actual_counts = { - category: len(case_ids) - for category, case_ids in selected_by_leaf.items() + category: len(case_ids) for category, case_ids in selected_by_leaf.items() } expected_counts = dict(suite.expected_leaf_counts) if actual_counts != expected_counts: @@ -602,8 +561,7 @@ def _read_selected_suite( case_ids_by_category[category] = tuple(case_ids) shape = tuple( - (category, len(case_ids)) - for category, case_ids in case_ids_by_category.items() + (category, len(case_ids)) for category, case_ids in case_ids_by_category.items() ) for suite in SUITE_SPECS.values(): if shape != suite.expected_leaf_counts: @@ -621,8 +579,6 @@ def _run_upstream( base_url: str, api_key: str, num_threads: int, - request_timeout_seconds: float, - request_max_retries: int, ) -> None: """Lazily load and invoke the pinned BFCL API against an existing server.""" suite, case_ids_by_category = _read_selected_suite(project_root) @@ -630,37 +586,12 @@ def _run_upstream( os.environ["OPENAI_BASE_URL"] = base_url os.environ["OPENAI_API_KEY"] = api_key - if suite.maximum_step_limit is not None: - import bfcl_eval.constants.default_prompts as bfcl_prompts - - bfcl_prompts.MAXIMUM_STEP_LIMIT = suite.maximum_step_limit import bfcl_eval.constants.model_config as bfcl_model_config from bfcl_eval.__main__ import evaluate, generate from bfcl_eval.constants.model_config import ModelConfig from bfcl_eval.model_handler.api_inference.openai_completion import ( OpenAICompletionsHandler, ) - request_failures: SimpleQueue[Exception] = SimpleQueue() - - class BoundedOpenAICompletionsHandler(OpenAICompletionsHandler): - def _build_client_kwargs(self) -> dict[str, Any]: - kwargs = super()._build_client_kwargs() - kwargs.update( - timeout=request_timeout_seconds, - max_retries=request_max_retries, - ) - return kwargs - - def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: - # Replace upstream's unbounded RateLimitError decorator with the - # OpenAI client's bounded transport and server-error retries. - started = time.monotonic() - try: - response = self.client.chat.completions.create(**kwargs) - except Exception as exc: - request_failures.put(exc) - raise - return response, time.monotonic() - started bfcl_model_config.MODEL_CONFIG_MAPPING[model] = ModelConfig( model_name=model, @@ -668,7 +599,7 @@ def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: url="", org="", license="unknown", - model_handler=BoundedOpenAICompletionsHandler, + model_handler=OpenAICompletionsHandler, input_price=None, output_price=None, is_fc_model=True, @@ -687,8 +618,6 @@ def generate_with_backoff(self, **kwargs: Any) -> tuple[Any, float]: allow_overwrite=True, ) generate(**generation_kwargs) - if not request_failures.empty(): - raise request_failures.get() _validate_generated_results(project_root, case_ids_by_category) evaluation_kwargs = _function_defaults(evaluate) @@ -890,8 +819,6 @@ def publish_integration_error( model=model, base_url=None, num_threads=suite.default_num_threads, - request_timeout_seconds=DEFAULT_REQUEST_TIMEOUT_SECONDS, - request_max_retries=DEFAULT_REQUEST_MAX_RETRIES, scores=None, integration_error=error, ), @@ -917,8 +844,6 @@ def run_evaluation( bfcl_project_root: Path, suite: SuiteSpec = SMOKE_SUITE, num_threads: int | None = None, - request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, - request_max_retries: int = DEFAULT_REQUEST_MAX_RETRIES, upstream_runner: UpstreamRunner = _run_upstream, ) -> bool: """Run one immutable BFCL suite and always publish both report formats.""" @@ -943,19 +868,6 @@ def run_evaluation( raise ValueError("num_threads must be a positive integer") if resolved_num_threads <= 0: raise ValueError("num_threads must be a positive integer") - if ( - isinstance(request_timeout_seconds, bool) - or not isinstance(request_timeout_seconds, (int, float)) - or not math.isfinite(float(request_timeout_seconds)) - or request_timeout_seconds <= 0 - ): - raise ValueError("request_timeout_seconds must be positive and finite") - if ( - isinstance(request_max_retries, bool) - or not isinstance(request_max_retries, int) - or request_max_retries < 0 - ): - raise ValueError("request_max_retries must be a non-negative integer") if not callable(upstream_runner): raise TypeError("upstream_runner must be callable") @@ -970,8 +882,6 @@ def run_evaluation( base_url=normalized_url, api_key=normalized_key, num_threads=resolved_num_threads, - request_timeout_seconds=float(request_timeout_seconds), - request_max_retries=request_max_retries, ) _validate_generated_results(bfcl_project_root, selected_case_ids) scores = _collect_scores(bfcl_project_root, selected_case_ids) @@ -988,8 +898,6 @@ def run_evaluation( model=model, base_url=base_url, num_threads=resolved_num_threads, - request_timeout_seconds=float(request_timeout_seconds), - request_max_retries=request_max_retries, scores=None, integration_error=exc, ), @@ -1012,8 +920,6 @@ def run_evaluation( model=normalized_model, base_url=normalized_url, num_threads=resolved_num_threads, - request_timeout_seconds=float(request_timeout_seconds), - request_max_retries=request_max_retries, scores=scores, ) compatibility = _compatibility_result( @@ -1042,16 +948,6 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: default=TASK_NAME, ) parser.add_argument("--num-threads", type=_positive_int) - parser.add_argument( - "--request-timeout-seconds", - type=_positive_float, - default=DEFAULT_REQUEST_TIMEOUT_SECONDS, - ) - parser.add_argument( - "--request-max-retries", - type=_nonnegative_int, - default=DEFAULT_REQUEST_MAX_RETRIES, - ) parser.add_argument("--integration-error") args = parser.parse_args(argv) args.num_threads = ( @@ -1089,8 +985,6 @@ def main(argv: Sequence[str] | None = None) -> int: bfcl_project_root=args.bfcl_project_root, suite=suite, num_threads=args.num_threads, - request_timeout_seconds=args.request_timeout_seconds, - request_max_retries=args.request_max_retries, ) return 0 if passed else 1 diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 09adf66f6b..4482bd21fa 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -68,15 +68,6 @@ def build_pytest_command( "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", *parallel_args, - "--reruns", - "6", - "--reruns-delay", - "3", - "--only-rerun", - ( - r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" - r"APITimeoutError|Connection error|timed out)" - ), "--base-url", base_url, "--api-key", @@ -99,12 +90,14 @@ def _mapping(value: Any, name: str) -> Mapping[str, Any]: raise ValueError(f"{name} must be an object") return value + def _expected_total(task_name: str) -> int: try: return EXPECTED_TOTALS[task_name] except KeyError as exc: raise ValueError(f"unsupported Kimi task: {task_name}") from exc + def _endpoint_rejection_messages(report: Any) -> list[str]: """Return upstream failures rejected before argument-schema validation.""" root = _mapping(report, "report") @@ -179,16 +172,13 @@ def _project_report( or line < 1 or not isinstance(selection_reason, str) ): - raise ValueError( - f"report.selected_cases[{index}] has invalid identity" - ) + raise ValueError(f"report.selected_cases[{index}] has invalid identity") selected_key = (suite, line, selection_reason) if selected_key in selected_keys: raise ValueError("report.selected_cases contains a duplicate case") selected_keys.add(selected_key) selected_identities.add((suite, line)) - modes: list[str] = [] case_modes: dict[tuple[str, int], set[str]] = {} result_passes = 0 @@ -236,7 +226,9 @@ def _project_report( if task_name == TASK_NAME: if set(modes) != EXPECTED_MODES or len(modes) != len(set(modes)): raise ValueError("report does not contain the expected stream modes") - elif any(modes_for_case != EXPECTED_MODES for modes_for_case in case_modes.values()): + elif any( + modes_for_case != EXPECTED_MODES for modes_for_case in case_modes.values() + ): raise ValueError( "report does not contain exactly one of each stream mode " "for every selected suite and line" @@ -297,6 +289,7 @@ def _compatibility_result( } return result + def _write_native_failure( path: Path, *, @@ -448,7 +441,6 @@ def _positive_int(value: str) -> int: return parsed - def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run the pinned stock Kimi Vendor Verifier tool-schema evaluation." diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 463f786a30..1e9a3b7670 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -1,126 +1,57 @@ #!/usr/bin/env python3 -"""Run the pinned single-case MiniMax M3 provider compatibility smoke.""" +"""Run a pinned MiniMax M3 smoke subset through the stock provider verifier.""" from __future__ import annotations import argparse import hashlib -import http.client import json import math -import re -import time -import urllib.error +import os +import subprocess import urllib.parse -import urllib.request from collections.abc import Callable, Mapping, Sequence from datetime import datetime, timezone from pathlib import Path from typing import Any +from minimax_m3_full_eval import UPSTREAM_REF, verify_source_tree + TASK_NAME = "minimax_m3_smoke" NATIVE_REPORT_FILENAME = "minimax_vendor_report.json" +NATIVE_RESULTS_FILENAME = "minimax_vendor_results.jsonl" COMPATIBILITY_GLOB = "results_minimax_vendor_*.json" DEFAULT_FIXTURE_PATH = Path(__file__).with_name("minimax_m3_smoke.json") -DEFAULT_REQUEST_TIMEOUT_SECONDS = 180.0 -DEFAULT_TIMEOUT_SECONDS = 900.0 -M3_DEFAULT_MAX_TOKENS = 40960 RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "minimax-provider-verifier" EXPECTED_INDICES = (71,) -M3_MODEL_REGEX = re.compile(r"(? None: - return None +Runner = Callable[..., subprocess.CompletedProcess[Any]] -_NO_REDIRECT_OPENER = urllib.request.build_opener(_RejectRedirects()) +class SmokeSuiteError(RuntimeError): + """The stock verifier could not produce one complete smoke result.""" def _mapping(value: Any, name: str) -> Mapping[str, Any]: if not isinstance(value, Mapping): - raise TypeError(f"{name} must be an object") + raise ValueError(f"{name} must be an object") return value -def _positive_number(value: Any, name: str) -> float: - if ( - isinstance(value, bool) - or not isinstance(value, (int, float)) - or not math.isfinite(value) - or value <= 0 - ): - raise ValueError(f"{name} must be a positive finite number") - return float(value) - - -def _positive_float(value: str) -> float: - try: - return _positive_number(float(value), "value") - except (TypeError, ValueError) as exc: - raise argparse.ArgumentTypeError("must be a positive finite number") from exc - - -def _validate_messages(value: Any, name: str) -> None: - if not isinstance(value, list) or not value: - raise ValueError(f"{name} must be a non-empty array") - for index, message in enumerate(value): - item = _mapping(message, f"{name}[{index}]") - if not isinstance(item.get("role"), str) or not isinstance( - item.get("content"), str - ): - raise TypeError(f"{name}[{index}] must contain string role and content") - - -def _validate_tools(value: Any, name: str) -> None: - if not isinstance(value, list) or not value: - raise ValueError(f"{name} must be a non-empty array") - for index, tool in enumerate(value): - item = _mapping(tool, f"{name}[{index}]") - function = _mapping(item.get("function"), f"{name}[{index}].function") - if item.get("type") != "function" or not isinstance(function.get("name"), str): - raise ValueError(f"{name}[{index}] must define a named function") - parameters = _mapping( - function.get("parameters"), f"{name}[{index}].function.parameters" - ) - _mapping( - parameters.get("properties"), - f"{name}[{index}].function.parameters.properties", - ) - - def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """Load and validate the exact pinned single-case fixture.""" + """Load the exact pinned row without modifying its request fields.""" root = _mapping(json.loads(path.read_text(encoding="utf-8")), "fixture") if root.get("source") != UPSTREAM_SOURCE or root.get("ref") != UPSTREAM_REF: raise ValueError("fixture source or ref does not match the pinned upstream") @@ -134,39 +65,23 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: raise ValueError("fixture must preserve the complete upstream MIT notice") raw_rows = root.get("rows") - if not isinstance(raw_rows, list) or len(raw_rows) != len(EXPECTED_INDICES): + if not isinstance(raw_rows, list) or len(raw_rows) != 1: raise ValueError("fixture must contain exactly one row") - - rows: list[dict[str, Any]] = [] - for position, raw_row in enumerate(raw_rows): - row = dict(_mapping(raw_row, f"fixture.rows[{position}]")) - data_index = row.get("data_index") - if data_index != EXPECTED_INDICES[position]: - raise ValueError("fixture rows must retain upstream order and data_index") - case_digest = hashlib.sha256( - json.dumps( - row, - ensure_ascii=False, - separators=(",", ":"), - ).encode() - ).hexdigest() - if case_digest != EXPECTED_CASE_SHA256[data_index]: - raise ValueError(f"fixture row {data_index} differs from pinned upstream") - _validate_messages(row.get("messages"), f"fixture.rows[{position}].messages") - if row.get("check_type", []) != []: - raise ValueError(f"fixture row {data_index} has unexpected check_type") - _validate_tools(row.get("tools"), f"fixture.rows[{position}].tools") - if row.get("expected_tool_call") is not True: - raise ValueError(f"fixture row {data_index} has an invalid expected label") - rows.append(row) - - return dict(root), rows - - -def build_endpoint(base_url: str) -> str: - if not isinstance(base_url, str) or not base_url.strip(): + row = dict(_mapping(raw_rows[0], "fixture.rows[0]")) + if row.get("data_index") != EXPECTED_INDICES[0]: + raise ValueError("fixture row must retain data_index 71") + digest = hashlib.sha256( + json.dumps(row, ensure_ascii=False, separators=(",", ":")).encode() + ).hexdigest() + if digest != EXPECTED_CASE_SHA256[EXPECTED_INDICES[0]]: + raise ValueError("fixture row 71 differs from pinned upstream") + return dict(root), [row] + + +def _normalized_base_url(value: str) -> str: + if not isinstance(value, str) or not value.strip(): raise ValueError("base_url must be a non-empty string") - normalized = base_url.strip().rstrip("/") + normalized = value.strip().rstrip("/") parsed = urllib.parse.urlsplit(normalized) if ( parsed.scheme not in {"http", "https"} @@ -177,475 +92,86 @@ def build_endpoint(base_url: str) -> str: raise ValueError( "base_url must be an absolute HTTP(S) URL without query or fragment" ) - return f"{normalized}/chat/completions" - - -def prepare_request(row: Mapping[str, Any], model: str) -> dict[str, Any]: - """Strip evaluator fields and apply the fixed smoke sampling overrides.""" - if not isinstance(model, str) or not model.strip(): - raise ValueError("model must be a non-empty string") - request = dict(row) - for field in ("data_index", "check_type", "expected_tool_call", "scenario_check"): - request.pop(field, None) - request.update( - model=model, - temperature=0, - top_p=1, - ) - if M3_MODEL_REGEX.search(model): - request["max_tokens"] = M3_DEFAULT_MAX_TOKENS - return request - - -def _read_response_body(response: Any, deadline: float) -> bytes: - chunks: list[bytes] = [] - total = 0 - read_chunk = getattr(response, "read1", response.read) - while True: - remaining = deadline - time.monotonic() - if remaining <= 0: - raise TimeoutError("chat completion response exceeded its deadline") - sock = getattr( - getattr(getattr(response, "fp", None), "raw", None), - "_sock", - None, - ) - if sock is not None: - sock.settimeout(remaining) - chunk = read_chunk(64 * 1024) - if not chunk: - return b"".join(chunks) - chunks.append(chunk) - total += len(chunk) - if total > MAX_RESPONSE_BYTES: - raise ValueError( - f"chat completion response exceeds {MAX_RESPONSE_BYTES} bytes" - ) - - -def _default_http_post( - *, - url: str, - headers: Mapping[str, str], - payload: Mapping[str, Any], - timeout_seconds: float, -) -> Any: - deadline = time.monotonic() + timeout_seconds - request = urllib.request.Request( - url, - data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), - headers=dict(headers), - method="POST", - ) - try: - with _NO_REDIRECT_OPENER.open(request, timeout=timeout_seconds) as response: - content = _read_response_body(response, deadline).decode("utf-8") - except urllib.error.HTTPError as exc: - if exc.code in {404, 429} or 500 <= exc.code < 600: - raise TransportError(f"HTTP {exc.code}: {exc.reason}") from exc - raise ValueError( - f"chat completion request failed with HTTP {exc.code}: {exc.reason}" - ) from exc - except ( - urllib.error.URLError, - http.client.HTTPException, - TimeoutError, - OSError, - ) as exc: - raise TransportError(str(exc)) from exc - try: - return json.loads(content) - except (UnicodeDecodeError, json.JSONDecodeError) as exc: - raise ValueError(f"chat completion response is not valid JSON: {exc}") from exc + return normalized -def _validate_chat_completion_response(value: Any) -> Mapping[str, Any]: - response = _mapping(value, "chat completion response") - choices = response.get("choices") - if not isinstance(choices, list) or not choices: - raise ValueError( - "chat completion response must contain a non-empty choices array" - ) - choice = _mapping(choices[0], "chat completion response.choices[0]") - if not isinstance(choice.get("finish_reason"), str): - raise TypeError("chat completion response must contain a finish_reason") - message = _mapping( - choice.get("message"), - "chat completion response.choices[0].message", +def prepare_smoke_input(*, fixture_path: Path, destination: Path) -> None: + """Write the pinned row as stock verifier JSONL input.""" + _, rows = load_fixture(fixture_path) + destination.write_text( + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows), + encoding="utf-8", ) - content = message.get("content") - if content is not None and not isinstance(content, str): - raise TypeError("chat completion message content must be a string or null") - tool_calls = message.get("tool_calls") - if tool_calls is not None and not isinstance(tool_calls, list): - raise TypeError("chat completion message tool_calls must be an array") - if choice["finish_reason"] == "tool_calls": - if not tool_calls: - raise ValueError( - "tool_calls finish reason requires at least one message tool call" - ) - elif tool_calls: - raise ValueError("message tool calls require a tool_calls finish reason") - return response - - -def _require_tool_validation() -> None: - """Fail setup before requests if the pinned schema dependency is unavailable.""" - try: - import jsonschema # noqa: F401 - except ImportError as exc: - raise RuntimeError("jsonschema is required for tool-call validation") from exc - - -def validate_tool_call(tool_call: Any, tools: list[dict[str, Any]]) -> bool: - """Apply pinned JSON Schema validation.""" - from jsonschema import ValidationError, validate - - try: - call = _mapping(tool_call, "tool_call") - function = _mapping(call["function"], "tool_call.function") - tool_name = function["name"] - schema = next( - ( - tool["function"]["parameters"] - for tool in tools - if tool["function"]["name"] == tool_name - ), - None, - ) - if not schema: - return False - args = function["arguments"] - if isinstance(args, str): - args = json.loads(args) - validate(instance=args, schema=schema) - return True - except (json.JSONDecodeError, ValidationError): - return False - except Exception: # noqa: BLE001 - upstream data can fail in arbitrary shapes - return False - - -def validate_tool_calls( - request: dict[str, Any], response: Any, status: str -) -> dict[str, Any]: - result: dict[str, Any] = { - "tool_calls_finish_reason": None, - "tool_calls_valid": None, - "tool_calls_count": 0, - } - if status != "success" or not response or "choices" not in response: - return result - choice = response["choices"][0] if response["choices"] else {} - finish_reason = choice.get("finish_reason") - result["tool_calls_finish_reason"] = finish_reason - if finish_reason == "tool_calls": - tools = request.get("tools", []) - tool_calls = choice.get("message", {}).get("tool_calls", []) - result["tool_calls_count"] = len(tool_calls) - if tool_calls: - result["tool_calls_valid"] = all( - validate_tool_call(tool_call, tools) for tool_call in tool_calls - ) - else: - result["tool_calls_valid"] = False - return result - - -# Adapted verbatim from pinned verify.py::_is_error_only_reasoning_response. -def _is_error_only_reasoning_response(response: Any) -> bool: - try: - if not response or "choices" not in response or not response["choices"]: - return False - message = response["choices"][0].get("message") or {} - reasoning = message.get("reasoning") or "" - content = message.get("content") or "" - tool_calls = message.get("tool_calls") - if isinstance(tool_calls, list): - has_tool_calls = len(tool_calls) > 0 - else: - has_tool_calls = bool(tool_calls) - return bool(reasoning) and (not content) and (not has_tool_calls) - except Exception: # noqa: BLE001 - mirrors the pinned upstream guard - return False - - -def _choice_fields(response: Any) -> tuple[Any, Any]: - if not isinstance(response, Mapping): - return None, None - choices = response.get("choices") - if not isinstance(choices, list) or not choices: - return None, None - choice = choices[0] - if not isinstance(choice, Mapping): - return None, None - message = choice.get("message") - content = message.get("content") if isinstance(message, Mapping) else None - return choice.get("finish_reason"), content - - -def _error_dict(exc: BaseException) -> dict[str, str]: - return {"type": type(exc).__name__, "message": str(exc)} - -def _evaluate_case( +def build_verifier_command( *, - row: dict[str, Any], + python: Path, + source_dir: Path, + sample_path: Path, + base_url: str, model: str, - endpoint: str, - api_key: str, - request_timeout_seconds: float, - deadline: float, - http_post: HttpPost, - clock: Clock, - sleeper: Sleeper, -) -> dict[str, Any]: - prepared = prepare_request(row, model) - started = clock() - response: Any = None - status = "failed" - attempts = 0 - request_error: BaseException | None = None - suite_timed_out = False - - for attempt in range(MAX_ATTEMPTS): - remaining = deadline - clock() - if remaining <= 0: - request_error = SuiteTimeoutError("global suite timeout exceeded") - suite_timed_out = True - break - attempts += 1 - try: - raw_response = http_post( - url=endpoint, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - payload=prepared, - timeout_seconds=min(request_timeout_seconds, remaining), - ) - if deadline - clock() <= 0: - request_error = SuiteTimeoutError("global suite timeout exceeded") - response = None - suite_timed_out = True - break - response = dict(_validate_chat_completion_response(raw_response)) - status = "success" - request_error = None - break - except (TransportError, TimeoutError, OSError) as exc: - if deadline - clock() <= 0: - request_error = SuiteTimeoutError("global suite timeout exceeded") - suite_timed_out = True - break - request_error = exc - if attempt + 1 < MAX_ATTEMPTS: - delay = min(RETRY_BACKOFF_SECONDS[attempt], deadline - clock()) - if delay > 0: - sleeper(delay) - continue - break - except Exception as exc: # noqa: BLE001 - preserve per-request diagnostics - request_error = exc - if deadline - clock() <= 0: - request_error = SuiteTimeoutError("global suite timeout exceeded") - suite_timed_out = True - break - - finish_reason, _ = _choice_fields(response) - result: dict[str, Any] = { - "data_index": row["data_index"], - "status": status, - "attempts": attempts, - "duration_ms": round(max(0.0, clock() - started) * 1000, 3), - "expected_tool_call": row.get("expected_tool_call"), - "finish_reason": finish_reason, - "response": response - if response is not None - else {"error": _error_dict(request_error or RuntimeError("request failed"))}, - "error_only_reasoning_checked": 1, - "error_only_reasoning": _is_error_only_reasoning_response(response), - "integration_failure": isinstance( - request_error, (TransportError, TimeoutError, OSError) - ), - } - - try: - result.update(validate_tool_calls(prepared, response, status)) - except Exception as exc: # noqa: BLE001 - validators must not abort the report - result["validator_error"] = _error_dict(exc) - - failures: list[str] = [] - if status != "success": - failures.append("query_failed") - if result["error_only_reasoning"]: - failures.append("error_only_reasoning") - expected_tool_call = row.get("expected_tool_call") - if isinstance(expected_tool_call, bool): - expected_finish_reason = "tool_calls" if expected_tool_call else "stop" - actual_tool_call = finish_reason == "tool_calls" - if finish_reason != expected_finish_reason: - failures.append("tool_call_trigger") - if ( - expected_tool_call - and actual_tool_call - and result.get("tool_calls_valid") is not True - ): - failures.append("tool_call_schema") - if "validator_error" in result: - failures.append("validator_error") - - result["case_passed"] = not failures - result["failures"] = failures - result["suite_timed_out"] = suite_timed_out - return result - - -def _ratio(numerator: int, denominator: int) -> float: - return numerator / denominator if denominator else 0.0 - + output_dir: Path, +) -> list[str]: + """Build one stock verify.py invocation over the pinned smoke row.""" + if not isinstance(model, str) or not model.strip(): + raise ValueError("model must be a non-empty string") + extra_body = json.dumps( + {"temperature": 0, "top_p": 1, "max_tokens": 40960}, + separators=(",", ":"), + ) + return [ + str(python), + str(source_dir / "verify.py"), + str(sample_path), + "--model", + model, + "--base-url", + _normalized_base_url(base_url), + "--api-key", + "EMPTY", + "--concurrency", + "1", + "--output", + str(output_dir / NATIVE_RESULTS_FILENAME), + "--summary", + str(output_dir / NATIVE_REPORT_FILENAME), + "--timeout", + "600", + "--retries", + "3", + "--extra-body", + extra_body, + ] -def _summarize( - results: list[dict[str, Any]], -) -> tuple[dict[str, Any], dict[str, float]]: - total = len(results) - success_count = sum(result.get("status") == "success" for result in results) - passed_count = sum(result.get("case_passed") is True for result in results) - labeled = [ - result - for result in results - if result.get("expected_tool_call") is True - or result.get("expected_tool_call") is False - ] - true_positive = sum( - result["expected_tool_call"] is True - and result.get("finish_reason") == "tool_calls" - for result in labeled - ) - false_negative = sum( - result["expected_tool_call"] is True - and result.get("finish_reason") != "tool_calls" - for result in labeled - ) - false_positive = sum( - result["expected_tool_call"] is False - and result.get("finish_reason") == "tool_calls" - for result in labeled - ) - expected_tool_finish_stop = sum( - result["expected_tool_call"] is True and result.get("finish_reason") == "stop" - for result in labeled - ) - expected_stop_finish_stop = sum( - result["expected_tool_call"] is False and result.get("finish_reason") == "stop" - for result in labeled - ) - precision = _ratio(true_positive, true_positive + false_positive) - recall = _ratio(true_positive, true_positive + false_negative) - trigger_f1 = ( - 2 * precision * recall / (precision + recall) if precision + recall else 0.0 - ) - schema_successes = sum( - result.get("expected_tool_call") is True - and result.get("finish_reason") == "tool_calls" - and result.get("tool_calls_valid") is True - for result in labeled - ) +def _error_dict(error: BaseException) -> dict[str, str]: + return {"type": type(error).__name__, "message": str(error)} - language_checked = sum( - result.get("language_following_checked") is True for result in results - ) - language_valid = sum( - result.get("language_following_valid") is True for result in results - ) - scenario_checked = sum( - result.get("scenario_check_checked") is True for result in results - ) - scenario_valid = sum( - result.get("scenario_check_valid") is True for result in results - ) - reasoning_errors = sum( - result.get("error_only_reasoning") is True for result in results - ) - metrics = { - "Query-Success-Rate": _ratio(success_count, total), - "ToolCalls-Trigger-Similarity": trigger_f1, - "ToolCalls-Schema-Accuracy": _ratio(schema_successes, true_positive), - "Error-Only-Reasoning-Rate": _ratio(reasoning_errors, total), - "Language-Following-Success-Rate": _ratio(language_valid, language_checked), - "Scenario-Check-Pass-Rate": _ratio(scenario_valid, scenario_checked), - } - summary: dict[str, Any] = { - "total": total, - "passed_count": passed_count, - "failed_count": total - passed_count, - "success_count": success_count, - "failure_count": total - success_count, - "tool_calls_finish_tool_calls": true_positive, - "tool_calls_finish_stop": expected_tool_finish_stop, - "stop_finish_tool_calls": false_positive, - "stop_finish_stop": expected_stop_finish_stop, - "expected_tool_call_total_count": len(labeled), - "tool_calls_successful_count": schema_successes, - "tool_calls_schema_validation_error_count": true_positive - schema_successes, - "error_only_reasoning_checked_count": total, - "error_only_reasoning_count": reasoning_errors, - "language_following_checked_count": language_checked, - "language_following_valid_count": language_valid, - "language_following_invalid_count": language_checked - language_valid, - "scenario_check_checked_count": scenario_checked, - "scenario_check_valid_count": scenario_valid, - "scenario_check_invalid_count": scenario_checked - scenario_valid, - "overall_compatibility_score": _ratio(passed_count, len(EXPECTED_INDICES)), - } - return summary, metrics +def _rate(value: Any, name: str) -> float: + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + or not 0.0 <= value <= 1.0 + ): + raise SmokeSuiteError(f"native summary {name} must be a finite rate") + return float(value) -def _native_report( - *, - model: str, - endpoint: str | None, - fixture_metadata: Mapping[str, Any] | None, - results: list[dict[str, Any]], - completed: bool, - integration_error: BaseException | None = None, -) -> dict[str, Any]: - summary, metrics = _summarize(results) - report: dict[str, Any] = { - "verifier": ADAPTER_NAME, - "task": TASK_NAME, - "model": model, - "endpoint": endpoint, - "completed": completed, - "threshold": 1.0, - "sampling": { - "temperature": 0, - "top_p": 1, - "max_tokens": M3_DEFAULT_MAX_TOKENS, - }, - "source": { - "url": (fixture_metadata or {}).get("source", UPSTREAM_SOURCE), - "ref": (fixture_metadata or {}).get("ref", UPSTREAM_REF), - "indices": list(EXPECTED_INDICES), - }, - "summary": summary, - "metrics": metrics, - "results": results, - } - if integration_error is not None: - report["integration_error"] = _error_dict(integration_error) - return report +def _compatibility_path(output_dir: Path) -> Path: + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") + return output_dir / f"results_minimax_vendor_{timestamp}.json" def _compatibility_result( + *, model: str, score: float, - *, - n_samples: int, + effective: int, integration_error: BaseException | None = None, ) -> dict[str, Any]: result: dict[str, Any] = { @@ -662,13 +188,20 @@ def _compatibility_result( TASK_NAME: { "metric_list": [{"metric": "exact_match"}], "filter_list": [{"name": "strict-match"}], + "native_metrics": [ + "tool_calls_match_rate", + "tool_calls_schema_accuracy", + "error_only_reasoning_rate", + ], } }, "n-samples": { - TASK_NAME: { - "original": len(EXPECTED_INDICES), - "effective": n_samples, - } + TASK_NAME: {"original": 1, "effective": effective}, + }, + "source": { + "repository": "MiniMax-AI/MiniMax-Provider-Verifier", + "ref": UPSTREAM_REF, + "indices": list(EXPECTED_INDICES), }, } if integration_error is not None: @@ -676,245 +209,178 @@ def _compatibility_result( return result -def prepare_compatibility_path(output_dir: Path) -> Path: - for stale_path in output_dir.glob(COMPATIBILITY_GLOB): - stale_path.unlink() - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S.%f") - return output_dir / f"results_minimax_vendor_{timestamp}.json" - - def _write_json(path: Path, value: Mapping[str, Any]) -> None: path.write_text( json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) -def _integration_results(exc: BaseException) -> list[dict[str, Any]]: - return [ - { - "data_index": data_index, - "status": "failed", - "attempts": 0, - "expected_tool_call": True - if data_index == 71 - else False - if data_index == 101 - else None, - "finish_reason": None, - "response": {"error": _error_dict(exc)}, - "error_only_reasoning_checked": 1, - "error_only_reasoning": False, - "case_passed": False, - "failures": ["integration_error"], - "suite_timed_out": isinstance(exc, SuiteTimeoutError), - } - for data_index in EXPECTED_INDICES - ] - - -def _failed_case_result(row: Mapping[str, Any], exc: BaseException) -> dict[str, Any]: - return { - "data_index": row["data_index"], - "status": "failed", - "attempts": 0, - "expected_tool_call": row.get("expected_tool_call"), - "finish_reason": None, - "response": {"error": _error_dict(exc)}, - "error_only_reasoning_checked": 1, - "error_only_reasoning": False, - "case_passed": False, - "failures": ["adapter_error"], - "suite_timed_out": isinstance(exc, SuiteTimeoutError), - "integration_failure": True, - } +def project_native_artifacts(*, output_dir: Path, model: str) -> Path: + """Validate stock outputs and project only their published metrics.""" + report = _mapping( + json.loads((output_dir / NATIVE_REPORT_FILENAME).read_text(encoding="utf-8")), + "native summary", + ) + result_lines = ( + (output_dir / NATIVE_RESULTS_FILENAME).read_text(encoding="utf-8").splitlines() + ) + if len(result_lines) != 1 or not result_lines[0].strip(): + raise SmokeSuiteError("native results must contain exactly one row") + result = _mapping(json.loads(result_lines[0]), "native result") + if result.get("data_index") != EXPECTED_INDICES[0]: + raise SmokeSuiteError("native result must retain data_index 71") + if result.get("status") != "success": + raise SmokeSuiteError("native verifier reported a request failure") + if report.get("model") != model: + raise SmokeSuiteError("native summary model does not match the requested model") + if report.get("success_count") != 1 or report.get("failure_count") != 0: + raise SmokeSuiteError("native summary does not describe one successful request") + + match_rate = _rate(report.get("tool_calls_match_rate"), "tool_calls_match_rate") + schema_rate = _rate( + report.get("tool_calls_schema_accuracy"), "tool_calls_schema_accuracy" + ) + reasoning_error_rate = _rate( + report.get("error_only_reasoning_rate"), "error_only_reasoning_rate" + ) + score = min(match_rate, schema_rate, 1.0 - reasoning_error_rate) + compatibility_path = _compatibility_path(output_dir) + _write_json( + compatibility_path, + _compatibility_result(model=model, score=score, effective=1), + ) + return compatibility_path -def publish_integration_error( - *, output_dir: Path, model: str, error: BaseException -) -> None: - """Publish both required zero-score artifacts without loading jsonschema.""" +def publish_failure(*, output_dir: Path, model: str, error: BaseException) -> Path: + """Publish integration metadata without rewriting stock artifacts.""" output_dir.mkdir(parents=True, exist_ok=True) - native_path = output_dir / NATIVE_REPORT_FILENAME - native_path.unlink(missing_ok=True) - compatibility_path = prepare_compatibility_path(output_dir) + native_report_path = output_dir / NATIVE_REPORT_FILENAME + if not native_report_path.exists(): + _write_json( + native_report_path, + { + "verifier": ADAPTER_NAME, + "task": TASK_NAME, + "model": model, + "completed": False, + "source": {"ref": UPSTREAM_REF, "indices": list(EXPECTED_INDICES)}, + "integration_error": _error_dict(error), + }, + ) + compatibility_path = _compatibility_path(output_dir) _write_json( - native_path, - _native_report( + compatibility_path, + _compatibility_result( model=model, - endpoint=None, - fixture_metadata=None, - results=_integration_results(error), - completed=False, + score=0.0, + effective=0, integration_error=error, ), ) - _write_json( - compatibility_path, - _compatibility_result(model, 0.0, n_samples=0, integration_error=error), - ) + return compatibility_path def run_evaluation( *, + python: Path, + source_dir: Path, + dependency_dir: Path, base_url: str, - api_key: str, model: str, output_dir: Path, fixture_path: Path = DEFAULT_FIXTURE_PATH, - request_timeout_seconds: float = DEFAULT_REQUEST_TIMEOUT_SECONDS, - timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, - http_post: HttpPost = _default_http_post, - clock: Clock = time.monotonic, - sleeper: Sleeper = time.sleep, + runner: Runner = subprocess.run, ) -> bool: - """Run the pinned case and always publish both artifacts.""" + """Run the stock upstream verifier once, then project its native metrics.""" output_dir.mkdir(parents=True, exist_ok=True) - native_path = output_dir / NATIVE_REPORT_FILENAME - native_path.unlink(missing_ok=True) - compatibility_path = prepare_compatibility_path(output_dir) - + for filename in (NATIVE_REPORT_FILENAME, NATIVE_RESULTS_FILENAME): + (output_dir / filename).unlink(missing_ok=True) + for stale_path in output_dir.glob(COMPATIBILITY_GLOB): + stale_path.unlink() + smoke_input = output_dir / "minimax_vendor_smoke_input.jsonl" + smoke_input.unlink(missing_ok=True) try: - request_timeout = _positive_number( - request_timeout_seconds, "request_timeout_seconds" + verify_source_tree(source_dir) + prepare_smoke_input(fixture_path=fixture_path, destination=smoke_input) + command = build_verifier_command( + python=python, + source_dir=source_dir, + sample_path=smoke_input, + base_url=base_url, + model=model, + output_dir=output_dir, + ) + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(source_dir), str(dependency_dir)) + ) + environment["PYTHONNOUSERSITE"] = "1" + completed = runner( + command, + env=environment, + timeout=UPSTREAM_TIMEOUT_SECONDS, + check=False, ) - suite_timeout = _positive_number(timeout_seconds, "timeout_seconds") - if not callable(http_post) or not callable(clock) or not callable(sleeper): - raise TypeError("http_post, clock, and sleeper must be callable") - deadline = clock() + suite_timeout - if not isinstance(model, str) or not model.strip(): - raise ValueError("model must be a non-empty string") - if not isinstance(api_key, str) or not api_key: - raise ValueError("api_key must be a non-empty string") - endpoint = build_endpoint(base_url) - fixture_metadata, rows = load_fixture(fixture_path) - _require_tool_validation() + if completed.returncode != 0: + raise SmokeSuiteError( + f"pinned upstream verifier exited with code {completed.returncode}" + ) + project_native_artifacts(output_dir=output_dir, model=model) except ( OSError, - RuntimeError, ValueError, - TypeError, - json.JSONDecodeError, + SmokeSuiteError, + subprocess.TimeoutExpired, ) as exc: - _write_json( - native_path, - _native_report( - model=model, - endpoint=None, - fixture_metadata=None, - results=_integration_results(exc), - completed=False, - integration_error=exc, - ), - ) - _write_json( - compatibility_path, - _compatibility_result(model, 0.0, n_samples=0, integration_error=exc), - ) + publish_failure(output_dir=output_dir, model=model, error=exc) return False - - results: list[dict[str, Any]] = [] - for row in rows: - try: - result = _evaluate_case( - row=row, - model=model, - endpoint=endpoint, - api_key=api_key, - request_timeout_seconds=request_timeout, - deadline=deadline, - http_post=http_post, - clock=clock, - sleeper=sleeper, - ) - except Exception as exc: # noqa: BLE001 - continue and report every case - result = _failed_case_result(row, exc) - results.append(result) - - timed_out = any(result["suite_timed_out"] for result in results) - failed_integration = next( - (result for result in results if result["integration_failure"]), - None, - ) - integration_error: BaseException | None = None - if timed_out: - integration_error = SuiteTimeoutError("global suite timeout exceeded") - elif failed_integration is not None: - error = failed_integration["response"]["error"] - message = str(error.get("message", "request failed")) - if error.get("type") == "TransportError": - integration_error = TransportError(message) - elif error.get("type") in {"TimeoutError", "SuiteTimeoutError"}: - integration_error = TimeoutError(message) - else: - integration_error = RuntimeError( - f"{error.get('type', 'adapter error')}: {message}" - ) - completed = integration_error is None - native = _native_report( - model=model, - endpoint=endpoint, - fixture_metadata=fixture_metadata, - results=results, - completed=completed, - integration_error=integration_error, - ) - passed_count = native["summary"]["passed_count"] - effective = len(results) if completed else 0 - score = passed_count / len(EXPECTED_INDICES) if completed else 0.0 - compatibility = _compatibility_result( - model, - score, - n_samples=effective, - integration_error=integration_error, - ) - _write_json(native_path, native) - _write_json(compatibility_path, compatibility) - return completed and passed_count == len(EXPECTED_INDICES) + finally: + smoke_input.unlink(missing_ok=True) + return True def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Run the pinned single-case MiniMax M3 provider smoke." - ) - parser.add_argument("--base-url") - parser.add_argument("--api-key", default="EMPTY") - parser.add_argument("--model", required=True) - parser.add_argument("--output-dir", required=True, type=Path) - parser.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE_PATH) - parser.add_argument( - "--request-timeout-seconds", - type=_positive_float, - default=DEFAULT_REQUEST_TIMEOUT_SECONDS, - ) - parser.add_argument( - "--timeout-seconds", type=_positive_float, default=DEFAULT_TIMEOUT_SECONDS + description="Run the pinned MiniMax M3 smoke through stock verify.py." ) - parser.add_argument("--integration-error") - args = parser.parse_args(argv) - if args.integration_error is None and args.base_url is None: - parser.error("--base-url required unless --integration-error is provided") - return args + subparsers = parser.add_subparsers(dest="command", required=True) + + run = subparsers.add_parser("run") + run.add_argument("--python", required=True, type=Path) + run.add_argument("--source-dir", required=True, type=Path) + run.add_argument("--dependency-dir", required=True, type=Path) + run.add_argument("--base-url", required=True) + run.add_argument("--model", required=True) + run.add_argument("--output-dir", required=True, type=Path) + run.add_argument("--fixture", type=Path, default=DEFAULT_FIXTURE_PATH) + + failure = subparsers.add_parser("failure") + failure.add_argument("--model", required=True) + failure.add_argument("--output-dir", required=True, type=Path) + failure.add_argument("--message", required=True) + return parser.parse_args(argv) def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) - if args.integration_error is not None: - publish_integration_error( + if args.command == "failure": + publish_failure( output_dir=args.output_dir, model=args.model, - error=RuntimeError(args.integration_error), + error=SmokeSuiteError(args.message), ) return 0 - passed = run_evaluation( + completed = run_evaluation( + python=args.python, + source_dir=args.source_dir, + dependency_dir=args.dependency_dir, base_url=args.base_url, - api_key=args.api_key, model=args.model, output_dir=args.output_dir, fixture_path=args.fixture, - request_timeout_seconds=args.request_timeout_seconds, - timeout_seconds=args.timeout_seconds, ) - return 0 if passed else 1 + return 0 if completed else 1 if __name__ == "__main__": diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index e64b64ca83..359a2b2130 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -3,6 +3,7 @@ import os import subprocess import sys +from types import ModuleType from pathlib import Path from typing import Any @@ -34,7 +35,6 @@ def _score(output_dir: Path) -> float: return _compatibility(output_dir)["results"][be.TASK_NAME]["acc,none"] - def _category_score( category: str, total_count: int, @@ -56,6 +56,7 @@ def _category_score( total_count=total_count, ) + def test_thresholds_are_stdlib_readable_without_pyyaml(monkeypatch) -> None: real_import = builtins.__import__ @@ -70,15 +71,14 @@ def import_without_yaml(name, *args, **kwargs): assert thresholds["default"]["bfcl_smoke"] == 0.75 assert thresholds["default"]["bfcl_parallel"] == 0.0 + def test_score_validator_uses_declared_bfcl_metric( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: result = { "results": {"bfcl_smoke": {"acc,none": 0.8, "acc_stderr,none": 0.1}}, "configs": { - "bfcl_smoke": { - "metric_list": [{"metric": "acc", "aggregation": "mean"}] - } + "bfcl_smoke": {"metric_list": [{"metric": "acc", "aggregation": "mean"}]} }, "n-samples": {"bfcl_smoke": {"original": 4, "effective": 4}}, } @@ -219,8 +219,6 @@ def test_command_defaults_and_required_runtime_inputs(tmp_path: Path) -> None: assert args.base_url == "http://localhost:8000/v1" assert args.api_key == "EMPTY" assert args.num_threads == 4 - assert args.request_timeout_seconds == 180.0 - assert args.request_max_retries == 2 assert args.integration_error is None assert args.suite == be.TASK_NAME with pytest.raises(SystemExit): @@ -292,18 +290,9 @@ def run_evaluation(**kwargs: Any) -> bool: assert return_code == 0 assert invocation["suite"] is be.KIMI_SUITE assert invocation["num_threads"] == 16 - assert invocation["request_max_retries"] == 2 - -@pytest.mark.parametrize( - ("flag", "value"), - ( - ("--num-threads", "0"), - ("--request-timeout-seconds", "nan"), - ("--request-max-retries", "-1"), - ), -) +@pytest.mark.parametrize(("flag", "value"), (("--num-threads", "0"),)) def test_cli_rejects_invalid_positive_values( tmp_path: Path, flag: str, value: str ) -> None: @@ -363,8 +352,6 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( "base_url": "http://127.0.0.1:8000/v1", "api_key": "EMPTY", "num_threads": 4, - "request_timeout_seconds": 180.0, - "request_max_retries": 2, } assert json.loads( (project_root / "test_case_ids_to_generate.json").read_text(encoding="utf-8") @@ -374,13 +361,13 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( "parallel": ["parallel_1"], "irrelevance": ["irrelevance_0"], } - assert (project_root / be.UPSTREAM_LICENSE_FILENAME).read_text( - encoding="utf-8" - ).startswith(" Apache License") + assert ( + (project_root / be.UPSTREAM_LICENSE_FILENAME) + .read_text(encoding="utf-8") + .startswith(" Apache License") + ) attribution = json.loads( - (project_root / be.UPSTREAM_ATTRIBUTION_FILENAME).read_text( - encoding="utf-8" - ) + (project_root / be.UPSTREAM_ATTRIBUTION_FILENAME).read_text(encoding="utf-8") ) assert attribution == { "artifact": "BFCL-generated evaluation results", @@ -746,17 +733,10 @@ def stop_after_observing_root(suite: be.SuiteSpec): assert observed_roots == [str(project_root)] -@pytest.mark.parametrize( - ("suite", "expected_step_limit"), - ( - (be.MINIMAX_SUITE, None), - (be.KIMI_SUITE, 10), - ), -) +@pytest.mark.parametrize("suite", (be.MINIMAX_SUITE, be.KIMI_SUITE)) def test_full_suite_ids_use_exact_sorted_leaf_allocations( monkeypatch, suite: be.SuiteSpec, - expected_step_limit: int | None, ) -> None: multi_turn_leaves = ( "multi_turn_base", @@ -777,7 +757,6 @@ def test_full_suite_ids_use_exact_sorted_leaf_allocations( "parallel_multiple": 200, **{leaf: 75 for leaf in multi_turn_leaves}, } - observed_step_limits: list[int | None] = [] def load_dataset_entry(category: str) -> list[dict[str, str]]: return [ @@ -793,8 +772,7 @@ def parse_test_category_argument(categories: list[str]) -> list[str]: else categories ) - def load_helpers(maximum_step_limit): - observed_step_limits.append(maximum_step_limit) + def load_helpers(): return ( load_dataset_entry, parse_test_category_argument, @@ -805,18 +783,16 @@ def load_helpers(maximum_step_limit): selected = be._build_suite_case_ids(suite) - assert observed_step_limits == [expected_step_limit] - assert tuple( - (category, len(case_ids)) for category, case_ids in selected.items() - ) == suite.expected_leaf_counts - assert sum(map(len, selected.values())) == suite.expected_sample_count - assert all( - list(case_ids) == sorted(case_ids) for case_ids in selected.values() + assert ( + tuple((category, len(case_ids)) for category, case_ids in selected.items()) + == suite.expected_leaf_counts ) + assert sum(map(len, selected.values())) == suite.expected_sample_count + assert all(list(case_ids) == sorted(case_ids) for case_ids in selected.values()) if suite is be.KIMI_SUITE: - assert { - leaf: len(selected[leaf]) for leaf in multi_turn_leaves - } == {leaf: 60 for leaf in multi_turn_leaves} + assert {leaf: len(selected[leaf]) for leaf in multi_turn_leaves} == { + leaf: 60 for leaf in multi_turn_leaves + } assert all(selected[leaf][-1].endswith("_059") for leaf in multi_turn_leaves) @@ -869,8 +845,7 @@ def test_kimi_projects_namespaced_leaf_and_weighted_aggregate_scores() -> None: assert compatibility["results"]["bfcl_vllm_kimi"]["acc,none"] == 655 / 1240 assert ( - compatibility["results"]["bfcl_vllm_kimi_multi_turn"]["acc,none"] - == 105 / 240 + compatibility["results"]["bfcl_vllm_kimi_multi_turn"]["acc,none"] == 105 / 240 ) assert compatibility["n-samples"]["bfcl_vllm_kimi_multi_turn"] == { "original": 240, @@ -910,8 +885,6 @@ def test_selected_suite_integration_error_preserves_suite_identity( assert native["sampling"] == { "temperature": 0.001, "num_threads": 8, - "request_timeout_seconds": 180.0, - "request_max_retries": 2, } assert list(compatibility["results"]) == [ "bfcl_vllm_minimax_m3", @@ -929,12 +902,7 @@ def test_selected_suite_integration_error_preserves_suite_identity( def test_score_total_must_match_every_selected_id(tmp_path: Path) -> None: project_root = tmp_path / "bfcl" - score_path = ( - project_root - / "score" - / "model-a" - / "BFCL_v4_simple_python_score.json" - ) + score_path = project_root / "score" / "model-a" / "BFCL_v4_simple_python_score.json" score_path.parent.mkdir(parents=True) score_path.write_text( json.dumps({"accuracy": 1.0, "correct_count": 1, "total_count": 1}) + "\n", @@ -949,3 +917,78 @@ def test_score_total_must_match_every_selected_id(tmp_path: Path) -> None: project_root, {"simple_python": ("simple_python_0", "simple_python_1")}, ) + + +def test_upstream_registration_uses_exact_stock_openai_handler( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + project_root = tmp_path / "bfcl" + be._write_id_map(project_root, be.SMOKE_CASE_IDS) + model_config_mapping: dict[str, Any] = {} + + class ModelConfig: + def __init__(self, **kwargs: Any) -> None: + self.__dict__.update(kwargs) + + class OpenAICompletionsHandler: + pass + + def generate(**_: Any) -> None: + result_dir = project_root / "result" / "model-a" + result_dir.mkdir(parents=True) + for category, case_ids in be.SMOKE_CASE_IDS.items(): + (result_dir / f"BFCL_v4_{category}_result.json").write_text( + "".join( + json.dumps({"id": case_id, "result": []}) + "\n" + for case_id in case_ids + ), + encoding="utf-8", + ) + + def evaluate(**_: Any) -> None: + pass + + modules = { + "bfcl_eval": ModuleType("bfcl_eval"), + "bfcl_eval.constants": ModuleType("bfcl_eval.constants"), + "bfcl_eval.constants.model_config": ModuleType( + "bfcl_eval.constants.model_config" + ), + "bfcl_eval.model_handler": ModuleType("bfcl_eval.model_handler"), + "bfcl_eval.model_handler.api_inference": ModuleType( + "bfcl_eval.model_handler.api_inference" + ), + "bfcl_eval.model_handler.api_inference.openai_completion": ModuleType( + "bfcl_eval.model_handler.api_inference.openai_completion" + ), + "bfcl_eval.__main__": ModuleType("bfcl_eval.__main__"), + } + modules[ + "bfcl_eval.constants.model_config" + ].MODEL_CONFIG_MAPPING = model_config_mapping + modules["bfcl_eval.constants.model_config"].ModelConfig = ModelConfig + modules[ + "bfcl_eval.model_handler.api_inference.openai_completion" + ].OpenAICompletionsHandler = OpenAICompletionsHandler + modules["bfcl_eval.__main__"].generate = generate + modules["bfcl_eval.__main__"].evaluate = evaluate + for name, module in modules.items(): + if name in { + "bfcl_eval", + "bfcl_eval.constants", + "bfcl_eval.model_handler", + "bfcl_eval.model_handler.api_inference", + }: + module.__path__ = [] + monkeypatch.setitem(sys.modules, name, module) + monkeypatch.setattr(be, "_function_defaults", lambda _: {}) + + be._run_upstream( + model="model-a", + project_root=project_root, + base_url="http://127.0.0.1:8000/v1", + api_key="EMPTY", + num_threads=4, + ) + + assert model_config_mapping["model-a"].model_handler is OpenAICompletionsHandler diff --git a/utils/evals/test_kimi_vendor_eval.py b/utils/evals/test_kimi_vendor_eval.py index b8c63611de..9587483ab3 100644 --- a/utils/evals/test_kimi_vendor_eval.py +++ b/utils/evals/test_kimi_vendor_eval.py @@ -24,6 +24,7 @@ def _report(stream_status: str = "passed") -> dict[str, Any]: ], } + def _full_report(*, failed_records: int = 0) -> dict[str, Any]: selected_cases: list[dict[str, Any]] = [] results: list[dict[str, Any]] = [] @@ -42,9 +43,7 @@ def _full_report(*, failed_records: int = 0) -> dict[str, Any]: "suite": "TestSchema", "line": line, "mode": mode, - "status": ( - "failed" if len(results) < failed_records else "passed" - ), + "status": ("failed" if len(results) < failed_records else "passed"), } ) return { @@ -97,15 +96,6 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: "-m", "pytest", "tests/tool_call_json_schema/test_tool_call_json_schema.py", - "--reruns", - "6", - "--reruns-delay", - "3", - "--only-rerun", - ( - r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" - r"APITimeoutError|Connection error|timed out)" - ), "--base-url", "http://127.0.0.1:8000/v1", "--api-key", @@ -126,6 +116,7 @@ def test_builds_fixed_upstream_pytest_command(tmp_path: Path) -> None: str(report), ] + def test_builds_full_upstream_pytest_command(tmp_path: Path) -> None: report = tmp_path / kve.NATIVE_REPORT_FILENAME @@ -142,15 +133,6 @@ def test_builds_full_upstream_pytest_command(tmp_path: Path) -> None: "tests/tool_call_json_schema/test_tool_call_json_schema.py", "-n", "8", - "--reruns", - "6", - "--reruns-delay", - "3", - "--only-rerun", - ( - r"(?i)(Error code: (404|429|5[0-9]{2})|APIConnectionError|" - r"APITimeoutError|Connection error|timed out)" - ), "--base-url", "http://127.0.0.1:8000/v1", "--api-key", @@ -253,6 +235,7 @@ def fake_run( assert "lm_eval_version" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes + def test_full_report_projects_all_mode_records_and_defers_quality_gating( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -296,6 +279,7 @@ def fake_run( assert "integration_error" not in projected assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes + def test_full_report_classifies_endpoint_failures_as_integration_errors( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -310,9 +294,7 @@ def test_full_report_classifies_endpoint_failures_as_integration_errors( def fake_run( command: list[str], *, cwd: Path, check: bool, timeout: int ) -> SimpleNamespace: - Path(command[command.index("--tool-json-report") + 1]).write_bytes( - native_bytes - ) + Path(command[command.index("--tool-json-report") + 1]).write_bytes(native_bytes) return SimpleNamespace(returncode=1) monkeypatch.setattr(kve.subprocess, "run", fake_run) @@ -331,9 +313,10 @@ def fake_run( assert _score(output_dir, kve.FULL_TASK_NAME) == 0.0 assert _n_eff(output_dir, kve.FULL_TASK_NAME) == 0 assert projected["integration_error"]["type"] == "RuntimeError" - assert "endpoint request or response failure" in projected[ - "integration_error" - ]["message"] + assert ( + "endpoint request or response failure" + in projected["integration_error"]["message"] + ) assert (output_dir / kve.NATIVE_REPORT_FILENAME).read_bytes() == native_bytes diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 47489b2be9..556c8ed8cd 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -1,777 +1,250 @@ -import builtins -import io +from __future__ import annotations + import json -import re -import sys -import threading -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import subprocess from pathlib import Path -from typing import Any, Self - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parent)) +from typing import Any import minimax_provider_eval as mpe -def _language_response(content: str = "お正月に子どもへ渡します。") -> dict[str, Any]: - return { - "id": "language-response", - "choices": [ - { - "finish_reason": "stop", - "message": {"content": content, "reasoning": ""}, - } - ], - } - - -def _tool_response( - *, arguments: str | None = None, finish_reason: str = "tool_calls" -) -> dict[str, Any]: - if finish_reason != "tool_calls": - return { - "id": "tool-response", - "choices": [ - {"finish_reason": finish_reason, "message": {"content": "done"}} - ], - } - if arguments is None: - arguments = json.dumps( +def _native_outputs( + output_dir: Path, + *, + model: str = "MiniMax-M3", + status: str = "success", + match_rate: float = 1.0, + schema_rate: float = 1.0, + reasoning_error_rate: float = 0.0, +) -> None: + (output_dir / mpe.NATIVE_RESULTS_FILENAME).write_text( + json.dumps({"data_index": 71, "status": status}) + "\n", + encoding="utf-8", + ) + (output_dir / mpe.NATIVE_REPORT_FILENAME).write_text( + json.dumps( { - "patient_id": "P-009417", - "med_list_path": "/mnt/clinical/emr/patients/P-009417/med_list_v3.json", - "classification_scheme": "rxnorm_ingredient", - "overlap_policy": "current_only", - "strict_route_matching": True, - "include_otc": True, - "ignore_statuses": ["discontinued", "on_hold"], - "output_format": "detailed_json", + "model": model, + "success_count": 1 if status == "success" else 0, + "failure_count": 0 if status == "success" else 1, + "tool_calls_match_rate": match_rate, + "tool_calls_schema_accuracy": schema_rate, + "error_only_reasoning_rate": reasoning_error_rate, } ) - return { - "id": "tool-response", - "choices": [ - { - "finish_reason": "tool_calls", - "message": { - "content": None, - "tool_calls": [ - { - "id": "call-1", - "type": "function", - "function": { - "name": "flag_duplicate_therapies", - "arguments": arguments, - }, - } - ], - }, - } - ], - } - - -def _scenario_response( - content: str = "123, some-parameter, xyz, another-parameter", -) -> dict[str, Any]: - return { - "id": "scenario-response", - "choices": [{"finish_reason": "stop", "message": {"content": content}}], - } - - -def _response_for(payload: dict[str, Any]) -> dict[str, Any]: - tools = payload.get("tools", []) - if not tools: - return _language_response() - tool_name = tools[0]["function"]["name"] - if tool_name == "flag_duplicate_therapies": - return _tool_response() - assert tool_name == "example" - return _scenario_response() + + "\n", + encoding="utf-8", + ) def _compatibility(output_dir: Path) -> dict[str, Any]: paths = list(output_dir.glob(mpe.COMPATIBILITY_GLOB)) assert len(paths) == 1 - assert re.fullmatch( - r"results_minimax_vendor_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{6}\.json", - paths[0].name, - ) return json.loads(paths[0].read_text(encoding="utf-8")) -def _native(output_dir: Path) -> dict[str, Any]: - return json.loads( - (output_dir / mpe.NATIVE_REPORT_FILENAME).read_text(encoding="utf-8") - ) +def test_fixture_is_exact_pinned_upstream_row() -> None: + metadata, rows = mpe.load_fixture(mpe.DEFAULT_FIXTURE_PATH) - -def _score(output_dir: Path) -> float: - return _compatibility(output_dir)["results"][mpe.TASK_NAME][ - "exact_match,strict-match" - ] + assert metadata["ref"] == mpe.UPSTREAM_REF + assert metadata["indices"] == [71] + assert [row["data_index"] for row in rows] == [71] -def _run( - output_dir: Path, - post: Any = _response_for, - **kwargs: Any, -) -> bool: - def http_post(**request: Any) -> Any: - return post(request["payload"]) +def test_prepare_smoke_input_preserves_fixture_row(tmp_path: Path) -> None: + destination = tmp_path / "smoke.jsonl" - return mpe.run_evaluation( - base_url="http://127.0.0.1:8000/v1/", - api_key="secret", - model="MiniMax-M3", - output_dir=output_dir, - http_post=http_post, - **kwargs, + mpe.prepare_smoke_input( + fixture_path=mpe.DEFAULT_FIXTURE_PATH, + destination=destination, ) + _, rows = mpe.load_fixture(mpe.DEFAULT_FIXTURE_PATH) + assert [json.loads(line) for line in destination.read_text().splitlines()] == rows -def test_success_writes_complete_native_and_compatibility_reports( + +def test_build_command_invokes_stock_verifier_without_source_changes( tmp_path: Path, ) -> None: - invocations: list[dict[str, Any]] = [] - - def http_post(**request: Any) -> dict[str, Any]: - invocations.append(request) - return _response_for(request["payload"]) - + source_dir = tmp_path / "source" output_dir = tmp_path / "output" - assert mpe.run_evaluation( + sample_path = tmp_path / "smoke.jsonl" + + command = mpe.build_verifier_command( + python=Path("/venv/bin/python"), + source_dir=source_dir, + sample_path=sample_path, base_url="http://127.0.0.1:8000/v1/", - api_key="secret", model="MiniMax-M3", output_dir=output_dir, - http_post=http_post, ) - assert len(invocations) == 1 - assert [ - call["payload"].get("tools", [{}])[0].get("function", {}).get("name") - for call in invocations - ] == ["flag_duplicate_therapies"] - for call in invocations: - assert call["url"] == "http://127.0.0.1:8000/v1/chat/completions" - assert call["headers"]["Authorization"] == "Bearer secret" - assert call["timeout_seconds"] <= mpe.DEFAULT_REQUEST_TIMEOUT_SECONDS - assert call["payload"]["model"] == "MiniMax-M3" - assert call["payload"]["temperature"] == 0 - assert call["payload"]["top_p"] == 1 - assert call["payload"]["max_tokens"] == mpe.M3_DEFAULT_MAX_TOKENS - assert "data_index" not in call["payload"] - assert "check_type" not in call["payload"] - assert "expected_tool_call" not in call["payload"] - - native = _native(output_dir) - assert native["verifier"] == mpe.ADAPTER_NAME - assert native["task"] == mpe.TASK_NAME - assert native["completed"] is True - assert native["threshold"] == 1.0 - assert native["source"] == { - "url": mpe.UPSTREAM_SOURCE, - "ref": mpe.UPSTREAM_REF, - "indices": [71], - } - assert native["summary"]["total"] == 1 - assert native["summary"]["passed_count"] == 1 - assert native["summary"]["overall_compatibility_score"] == 1.0 - assert native["metrics"] == { - "Query-Success-Rate": 1.0, - "ToolCalls-Trigger-Similarity": 1.0, - "ToolCalls-Schema-Accuracy": 1.0, - "Error-Only-Reasoning-Rate": 0.0, - "Language-Following-Success-Rate": 0.0, - "Scenario-Check-Pass-Rate": 0.0, - } - assert [result["data_index"] for result in native["results"]] == [71] - assert [result["response"]["id"] for result in native["results"]] == [ - "tool-response", + assert command[:3] == [ + "/venv/bin/python", + str(source_dir / "verify.py"), + str(sample_path), ] - - compatibility = _compatibility(output_dir) - assert compatibility["result_format"] == mpe.RESULT_FORMAT - assert compatibility["eval_adapter"] == mpe.ADAPTER_NAME - assert compatibility["model_name"] == "MiniMax-M3" - assert _score(output_dir) == 1.0 - assert compatibility["n-samples"][mpe.TASK_NAME] == { - "original": 1, - "effective": 1, + assert command[command.index("--base-url") + 1] == "http://127.0.0.1:8000/v1" + assert command[command.index("--concurrency") + 1] == "1" + assert command[command.index("--timeout") + 1] == "600" + assert command[command.index("--retries") + 1] == "3" + assert json.loads(command[command.index("--extra-body") + 1]) == { + "temperature": 0, + "top_p": 1, + "max_tokens": 40960, } - assert "secret" not in json.dumps([native, compatibility]) -def test_non_m3_request_does_not_force_m3_token_budget() -> None: - _, rows = mpe.load_fixture(mpe.DEFAULT_FIXTURE_PATH) - - request = mpe.prepare_request(rows[0], "moonshotai/Kimi-K3") - - assert "max_tokens" not in request - - -def test_schema_failure_fails_only_tool_case(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - if payload.get("tools", [{}])[0].get("function", {}).get("name") == ( - "flag_duplicate_therapies" - ): - return _tool_response(arguments="{}") - return _response_for(payload) - - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - tool_result = native["results"][0] - assert tool_result["tool_calls_valid"] is False - assert tool_result["failures"] == ["tool_call_schema"] - assert native["metrics"]["ToolCalls-Schema-Accuracy"] == 0.0 - assert native["summary"]["passed_count"] == 0 - assert _score(output_dir) == 0.0 - - -def test_trigger_failure_uses_expected_label(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - if payload.get("tools", [{}])[0].get("function", {}).get("name") == ( - "flag_duplicate_therapies" - ): - return _tool_response(finish_reason="stop") - return _response_for(payload) - - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - assert native["results"][0]["failures"] == ["tool_call_trigger"] - assert native["metrics"]["ToolCalls-Trigger-Similarity"] == 0.0 - assert native["summary"]["tool_calls_finish_stop"] == 1 - assert native["summary"]["stop_finish_stop"] == 0 - - - - -def test_transport_retries_with_backoff_then_preserves_success( - tmp_path: Path, +def test_run_uses_verified_stock_source_and_projects_native_metrics( + tmp_path: Path, monkeypatch ) -> None: - attempts = 0 - sleeps: list[float] = [] - - def http_post(**request: Any) -> dict[str, Any]: - nonlocal attempts - attempts += 1 - if attempts == 1: - raise OSError("connection reset") - return _response_for(request["payload"]) - output_dir = tmp_path / "output" - assert mpe.run_evaluation( - base_url="https://provider.example/v1", - api_key="secret", - model="MiniMax-M3", - output_dir=output_dir, - http_post=http_post, - sleeper=sleeps.append, - ) - assert attempts == 2 - assert sleeps == [5.0] - assert _native(output_dir)["results"][0]["attempts"] == 2 - assert _score(output_dir) == 1.0 - - -@pytest.mark.parametrize( - ("status_code", "error_type"), - ( - (400, ValueError), - (404, mpe.TransportError), - (408, ValueError), - (429, mpe.TransportError), - (503, mpe.TransportError), - ), -) -def test_default_http_post_retries_only_retryable_http_statuses( - monkeypatch: pytest.MonkeyPatch, - status_code: int, - error_type: type[BaseException], -) -> None: - def fail_request(*args: Any, **kwargs: Any) -> Any: - raise mpe.urllib.error.HTTPError( - "https://provider.example/v1/chat/completions", - status_code, - "request failed", - {}, - io.BytesIO(b"provider rejected request"), + source_dir = tmp_path / "source" + dependency_dir = tmp_path / "deps" + source_dir.mkdir() + dependency_dir.mkdir() + verified: list[Path] = [] + invocation: dict[str, Any] = {} + + monkeypatch.setattr(mpe, "verify_source_tree", verified.append) + + def runner(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + invocation["command"] = command + invocation.update(kwargs) + _native_outputs( + output_dir, + match_rate=0.9, + schema_rate=0.8, + reasoning_error_rate=0.1, ) + return subprocess.CompletedProcess(command, 0) - monkeypatch.setattr(mpe._NO_REDIRECT_OPENER, "open", fail_request) - - with pytest.raises(error_type): - mpe._default_http_post( - url="https://provider.example/v1/chat/completions", - headers={"Authorization": "Bearer secret"}, - payload={"model": "MiniMax-M3", "messages": []}, - timeout_seconds=1, - ) - - -def test_default_http_post_maps_incomplete_body_to_transport_error( - monkeypatch: pytest.MonkeyPatch, -) -> None: - class IncompleteResponse: - def __enter__(self) -> Self: - return self - - def __exit__(self, *args: object) -> None: - return None - - def read(self, size: int) -> bytes: - return self.read1(size) - - def read1(self, size: int) -> bytes: - raise mpe.http.client.IncompleteRead(b'{"choices":', 100) - - monkeypatch.setattr( - mpe._NO_REDIRECT_OPENER, - "open", - lambda *args, **kwargs: IncompleteResponse(), + passed = mpe.run_evaluation( + python=Path("/venv/bin/python"), + source_dir=source_dir, + dependency_dir=dependency_dir, + base_url="http://127.0.0.1:8000/v1", + model="MiniMax-M3", + output_dir=output_dir, + runner=runner, ) - with pytest.raises(mpe.TransportError): - mpe._default_http_post( - url="https://provider.example/v1/chat/completions", - headers={"Authorization": "Bearer secret"}, - payload={"model": "MiniMax-M3", "messages": []}, - timeout_seconds=1, - ) - - -def test_default_http_post_rejects_redirect_without_leaking_authorization() -> None: - received_authorization: list[str | None] = [] - - class TargetHandler(BaseHTTPRequestHandler): - def record_request(self) -> None: - received_authorization.append(self.headers.get("Authorization")) - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(b'{"choices":[]}') - - do_GET = record_request - do_POST = record_request - - def log_message(self, *args: Any) -> None: - return None - - target_server = ThreadingHTTPServer(("127.0.0.1", 0), TargetHandler) - target_url = f"http://127.0.0.1:{target_server.server_address[1]}/credential-target" - - class RedirectHandler(BaseHTTPRequestHandler): - def do_POST(self) -> None: - self.send_response(302) - self.send_header("Location", target_url) - self.end_headers() + assert passed is True + assert verified == [source_dir] + assert invocation["check"] is False + assert invocation["timeout"] == mpe.UPSTREAM_TIMEOUT_SECONDS + assert invocation["env"]["PYTHONPATH"] == f"{source_dir}:{dependency_dir}" + assert invocation["env"]["PYTHONNOUSERSITE"] == "1" + assert not (output_dir / "minimax_vendor_smoke_input.jsonl").exists() + compatibility = _compatibility(output_dir) + assert compatibility["results"][mpe.TASK_NAME]["exact_match,strict-match"] == 0.8 + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 1 + assert "integration_error" not in compatibility - def log_message(self, *args: Any) -> None: - return None - redirect_server = ThreadingHTTPServer(("127.0.0.1", 0), RedirectHandler) - target_thread = threading.Thread(target=target_server.serve_forever, daemon=True) - redirect_thread = threading.Thread( - target=redirect_server.serve_forever, - daemon=True, - ) - target_thread.start() - redirect_thread.start() - try: - with pytest.raises(ValueError, match="HTTP 302"): - mpe._default_http_post( - url=( - f"http://127.0.0.1:{redirect_server.server_address[1]}" - "/v1/chat/completions" - ), - headers={"Authorization": "Bearer secret"}, - payload={"model": "MiniMax-M3", "messages": []}, - timeout_seconds=1, - ) - finally: - redirect_server.shutdown() - target_server.shutdown() - redirect_server.server_close() - target_server.server_close() - redirect_thread.join() - target_thread.join() - - assert received_authorization == [] - - -@pytest.mark.parametrize( - "malformed", - ( - None, - [], - {}, - {"choices": []}, - {"choices": [{}]}, - {"choices": [{"finish_reason": "stop"}]}, - { - "choices": [ - {"finish_reason": "stop", "message": {"content": {"not": "text"}}} - ] - }, - {"choices": [{"finish_reason": "tool_calls", "message": {"tool_calls": {}}}]}, - { - "choices": [ - { - "finish_reason": "stop", - "message": { - "content": "123, some-parameter, xyz, another-parameter", - "tool_calls": [{"id": "unexpected"}], - }, - } - ] - }, - ), -) -def test_malformed_chat_response_records_diagnostic_and_continues( - tmp_path: Path, malformed: Any +def test_completed_model_failure_is_not_reclassified_as_integration_error( + tmp_path: Path, monkeypatch ) -> None: - calls = 0 - - def post(payload: dict[str, Any]) -> Any: - nonlocal calls - calls += 1 - return malformed - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - assert calls == 1 - assert len(native["results"]) == 1 - for result in native["results"]: - assert result["status"] == "failed" - assert result["failures"][0] == "query_failed" - assert result["response"]["error"]["type"] in {"TypeError", "ValueError"} - - -def test_exhausted_transport_failure_publishes_report(tmp_path: Path) -> None: - calls = 0 - sleeps: list[float] = [] - - def http_post(**request: Any) -> dict[str, Any]: - nonlocal calls - calls += 1 - raise mpe.TransportError("offline") - - output_dir = tmp_path / "output" - assert not mpe.run_evaluation( - base_url="https://provider.example/v1", - api_key="secret", + source_dir = tmp_path / "source" + dependency_dir = tmp_path / "deps" + source_dir.mkdir() + dependency_dir.mkdir() + monkeypatch.setattr(mpe, "verify_source_tree", lambda _: None) + + def runner(command: list[str], **_: Any) -> subprocess.CompletedProcess[str]: + _native_outputs(output_dir, match_rate=0.0, schema_rate=0.0) + return subprocess.CompletedProcess(command, 0) + + passed = mpe.run_evaluation( + python=Path("python"), + source_dir=source_dir, + dependency_dir=dependency_dir, + base_url="http://127.0.0.1:8000/v1", model="MiniMax-M3", output_dir=output_dir, - http_post=http_post, - sleeper=sleeps.append, + runner=runner, ) - native = _native(output_dir) - assert calls == 4 - assert sleeps == [5.0, 10.0, 20.0] - assert len(native["results"]) == 1 - assert native["results"][0]["failures"] == [ - "query_failed", - "tool_call_trigger", - ] - assert native["metrics"]["Query-Success-Rate"] == 0.0 - assert native["completed"] is False - assert native["integration_error"] == { - "type": "TransportError", - "message": "offline", - } + + assert passed is True compatibility = _compatibility(output_dir) - assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 0 - assert compatibility["integration_error"] == native["integration_error"] + assert compatibility["results"][mpe.TASK_NAME]["exact_match,strict-match"] == 0.0 + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 1 + assert "integration_error" not in compatibility -def test_global_deadline_caps_attempts_and_publishes_timeout_report( - tmp_path: Path, +def test_request_failure_is_reported_as_integration_error( + tmp_path: Path, monkeypatch ) -> None: - now = [0.0] - timeouts: list[float] = [] - - def clock() -> float: - return now[0] - - def http_post(**request: Any) -> dict[str, Any]: - timeouts.append(request["timeout_seconds"]) - now[0] += 1.1 - return _response_for(request["payload"]) - output_dir = tmp_path / "output" - assert not mpe.run_evaluation( - base_url="https://provider.example/v1", - api_key="secret", + source_dir = tmp_path / "source" + dependency_dir = tmp_path / "deps" + source_dir.mkdir() + dependency_dir.mkdir() + monkeypatch.setattr(mpe, "verify_source_tree", lambda _: None) + + def runner(command: list[str], **_: Any) -> subprocess.CompletedProcess[str]: + _native_outputs(output_dir, status="failed") + return subprocess.CompletedProcess(command, 0) + + passed = mpe.run_evaluation( + python=Path("python"), + source_dir=source_dir, + dependency_dir=dependency_dir, + base_url="http://127.0.0.1:8000/v1", model="MiniMax-M3", output_dir=output_dir, - timeout_seconds=1.0, - request_timeout_seconds=180, - http_post=http_post, - clock=clock, + runner=runner, ) - native = _native(output_dir) - assert timeouts == pytest.approx([1.0]) - assert len(native["results"]) == 1 - assert native["results"][0]["suite_timed_out"] is True - assert native["completed"] is False - assert native["integration_error"]["type"] == "SuiteTimeoutError" + + assert passed is False compatibility = _compatibility(output_dir) - assert _score(output_dir) == 0.0 assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 0 - assert compatibility["integration_error"]["type"] == "SuiteTimeoutError" - - -def test_default_http_post_bounds_a_drip_feed_body_by_wall_clock( - monkeypatch: pytest.MonkeyPatch, -) -> None: - now = [0.0] - - class FakeSocket: - def __init__(self) -> None: - self.timeouts: list[float] = [] - - def settimeout(self, timeout: float) -> None: - self.timeouts.append(timeout) - - class DripResponse: - def __init__(self) -> None: - self.socket = FakeSocket() - self.fp = type( - "Raw", (), {"raw": type("Socket", (), {"_sock": self.socket})()} - )() - self.reads = 0 - - def __enter__(self) -> Self: - return self - - def __exit__(self, *args: object) -> None: - return None - - def read(self, size: int) -> bytes: - return self.read1(size) - - def read1(self, size: int) -> bytes: - self.reads += 1 - now[0] += 0.02 - return b"x" - - response = DripResponse() - monkeypatch.setattr(mpe.time, "monotonic", lambda: now[0]) - monkeypatch.setattr( - mpe._NO_REDIRECT_OPENER, - "open", - lambda *args, **kwargs: response, - ) + assert compatibility["integration_error"]["type"] == "SmokeSuiteError" + assert "request failure" in compatibility["integration_error"]["message"] + native = json.loads((output_dir / mpe.NATIVE_REPORT_FILENAME).read_text()) + assert native["failure_count"] == 1 + assert "integration_error" not in native - with pytest.raises(mpe.TransportError, match="deadline"): - mpe._default_http_post( - url="https://provider.example/v1/chat/completions", - headers={"Authorization": "Bearer secret"}, - payload={"model": "MiniMax-M3", "messages": []}, - timeout_seconds=0.05, - ) - assert response.reads == 3 - assert response.socket.timeouts == pytest.approx([0.05, 0.03, 0.01]) - - -def test_reasoning_only_response_is_always_checked(tmp_path: Path) -> None: - def post(payload: dict[str, Any]) -> dict[str, Any]: - return { - "choices": [ - { - "finish_reason": "length", - "message": { - "reasoning": "I could not answer", - "content": "", - "tool_calls": [], - }, - } - ] - } - - output_dir = tmp_path / "output" - assert not _run(output_dir, post) - native = _native(output_dir) - assert native["results"][0]["error_only_reasoning"] is True - assert native["results"][0]["failures"] == [ - "error_only_reasoning", - "tool_call_trigger", - ] - assert native["metrics"]["Error-Only-Reasoning-Rate"] == 1.0 - - -def test_stale_artifacts_are_removed_without_touching_foreign_results( +def test_publish_failure_does_not_replace_existing_native_report( tmp_path: Path, ) -> None: output_dir = tmp_path / "output" output_dir.mkdir() - (output_dir / mpe.NATIVE_REPORT_FILENAME).write_text("stale") - for stamp in ("2000-01-01T00-00-00.000000", "2001-01-01T00-00-00.000000"): - (output_dir / f"results_minimax_vendor_{stamp}.json").write_text("stale") - foreign = output_dir / "results_kimi_vendor_keep.json" - foreign.write_text("keep") + native_path = output_dir / mpe.NATIVE_REPORT_FILENAME + native_path.write_text('{"stock": true}\n', encoding="utf-8") - assert _run(output_dir) - assert len(list(output_dir.glob(mpe.COMPATIBILITY_GLOB))) == 1 - assert _native(output_dir)["completed"] is True - assert foreign.read_text() == "keep" + mpe.publish_failure( + output_dir=output_dir, + model="MiniMax-M3", + error=RuntimeError("transport failed"), + ) + assert json.loads(native_path.read_text()) == {"stock": True} + compatibility = _compatibility(output_dir) + assert compatibility["integration_error"] == { + "type": "RuntimeError", + "message": "transport failed", + } -def test_integration_error_cli_is_dependency_free_and_writes_both_artifacts( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - output_dir = tmp_path / "output" - real_import = builtins.__import__ - def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any: - if name == "jsonschema" or name.startswith("jsonschema."): - raise ImportError("jsonschema setup failed") - return real_import(name, *args, **kwargs) +def test_failure_cli_is_stdlib_only(tmp_path: Path) -> None: + output_dir = tmp_path / "output" - monkeypatch.setattr(builtins, "__import__", guarded_import) assert ( mpe.main( [ + "failure", "--model", "MiniMax-M3", "--output-dir", str(output_dir), - "--integration-error", - "dependency installation failed", + "--message", + "setup failed", ] ) == 0 ) - - native = _native(output_dir) - assert native["completed"] is False - assert len(native["results"]) == 1 - assert native["integration_error"] == { - "type": "RuntimeError", - "message": "dependency installation failed", - } compatibility = _compatibility(output_dir) - assert _score(output_dir) == 0.0 - assert compatibility["n-samples"][mpe.TASK_NAME] == { - "original": 1, - "effective": 0, - } - assert compatibility["integration_error"]["message"] == ( - "dependency installation failed" - ) - - -def test_invalid_runtime_input_writes_zero_score_artifacts(tmp_path: Path) -> None: - output_dir = tmp_path / "output" - called = False - - def http_post(**request: Any) -> Any: - nonlocal called - called = True - return _response_for(request["payload"]) - - assert not mpe.run_evaluation( - base_url="provider-without-a-scheme", - api_key="secret", - model="MiniMax-M3", - output_dir=output_dir, - http_post=http_post, - ) - assert called is False - assert _native(output_dir)["integration_error"]["type"] == "ValueError" - assert _score(output_dir) == 0.0 - assert _compatibility(output_dir)["n-samples"][mpe.TASK_NAME]["effective"] == 0 - -def test_missing_schema_dependency_is_an_integration_error( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - called = False - - def http_post(**request: Any) -> Any: - nonlocal called - called = True - return _response_for(request["payload"]) - - def missing_dependency() -> None: - raise RuntimeError("jsonschema is required for tool-call validation") - - monkeypatch.setattr(mpe, "_require_tool_validation", missing_dependency) - - assert not mpe.run_evaluation( - base_url="https://provider.example/v1", - api_key="secret", - model="MiniMax-M3", - output_dir=tmp_path, - http_post=http_post, - ) - assert called is False - native = _native(tmp_path) - assert native["completed"] is False - assert native["integration_error"]["type"] == "RuntimeError" - assert _compatibility(tmp_path)["n-samples"][mpe.TASK_NAME]["effective"] == 0 - - -@pytest.mark.parametrize( - "field", - ["indices", "ref", "license", "rows", "prompt", "tool_schema"], -) -def test_fixture_rejects_unpinned_or_incomplete_input( - tmp_path: Path, field: str -) -> None: - fixture = json.loads(mpe.DEFAULT_FIXTURE_PATH.read_text(encoding="utf-8")) - if field == "indices": - fixture[field] = [0] - elif field == "ref": - fixture[field] = "main" - elif field == "license": - fixture[field] = "MIT License" - elif field == "rows": - fixture[field] = [] - elif field == "prompt": - fixture["rows"][0]["messages"][0]["content"] = "changed" - else: - fixture["rows"][0]["tools"][0]["function"]["parameters"]["type"] = "array" - path = tmp_path / "fixture.json" - path.write_text(json.dumps(fixture), encoding="utf-8") - - with pytest.raises(ValueError): - mpe.load_fixture(path) - - -def test_cli_validates_required_url_and_positive_bounds(tmp_path: Path) -> None: - with pytest.raises(SystemExit): - mpe.parse_args(["--model", "MiniMax-M3", "--output-dir", str(tmp_path)]) - with pytest.raises(SystemExit): - mpe.parse_args( - [ - "--model", - "MiniMax-M3", - "--output-dir", - str(tmp_path), - "--base-url", - "https://provider.example/v1", - "--timeout-seconds", - "0", - ] - ) - with pytest.raises(SystemExit): - mpe.parse_args( - [ - "--model", - "MiniMax-M3", - "--output-dir", - str(tmp_path), - "--base-url", - "https://provider.example/v1", - "--request-timeout-seconds", - "nan", - ] - ) + assert compatibility["integration_error"]["message"] == "setup failed" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1ff9be2afb..ac3d0a2e7a 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -1,4 +1,3 @@ - from __future__ import annotations import hashlib @@ -27,7 +26,7 @@ REPO_ROOT / "benchmarks" / "single_node" / "agentic" / "qwen3.5_fp8_h200_mtp.sh", ) -_SCRIPT = r''' +_SCRIPT = r""" source "$BENCHMARK_LIB" run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } @@ -38,7 +37,7 @@ export EVAL_MAX_MODEL_LEN=16384 export EVAL_CONCURRENT_REQUESTS="" run_eval ${CLI_FW:+--framework "$CLI_FW"} --port 8888 -''' +""" def _dispatch( @@ -69,7 +68,6 @@ def _dispatch( return res.stdout - def test_agentic_scenario_defaults_to_gsm8k_lm_eval(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="1") @@ -77,6 +75,7 @@ def test_agentic_scenario_defaults_to_gsm8k_lm_eval(): def test_fixed_seqlen_scenario_defaults_to_lm_eval(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="0") + def test_agentic_eval_only_stages_summary(): output = _dispatch(is_agentic="1", eval_only="true") assert "DISPATCH=lm-eval" in output @@ -86,6 +85,7 @@ def test_agentic_eval_only_stages_summary(): def test_fixed_seqlen_eval_only_leaves_staging_to_recipe(): assert "STAGED=summary" not in _dispatch(is_agentic="0", eval_only="true") + def test_fixed_seqlen_provider_leaves_staging_to_recipe() -> None: output = _dispatch( is_agentic="0", @@ -96,7 +96,6 @@ def test_fixed_seqlen_provider_leaves_staging_to_recipe() -> None: assert "STAGED=summary" not in output - def test_explicit_framework_arg_overrides_scenario(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="1", cli_fw="lm-eval") @@ -104,6 +103,7 @@ def test_explicit_framework_arg_overrides_scenario(): def test_env_framework_overrides_scenario(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="1", env_fw="lm-eval") + def test_environment_framework_overrides_legacy_recipe_argument() -> None: assert "DISPATCH=kimi-vendor" in _dispatch( is_agentic="1", @@ -116,8 +116,6 @@ def test_env_can_force_swebench_on_fixed_seqlen(): assert "DISPATCH=swebench" in _dispatch(is_agentic="0", env_fw="swebench") - - def test_env_can_force_kimi_vendor_on_agentic_eval() -> None: assert "DISPATCH=kimi-vendor" in _dispatch( is_agentic="1", @@ -127,7 +125,7 @@ def test_env_can_force_kimi_vendor_on_agentic_eval() -> None: def test_kimi_vendor_skips_unused_model_context_loading() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" unset EVAL_MAX_MODEL_LEN compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } @@ -137,7 +135,7 @@ def test_kimi_vendor_skips_unused_model_context_loading() -> None: export EVAL_ONLY=false export IS_AGENTIC=0 run_eval --port 8888 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -152,7 +150,7 @@ def test_kimi_vendor_skips_unused_model_context_loading() -> None: def test_kimi_failure_preserves_rc_without_eval_only() -> None: - script = r''' + script = r""" set -u source "$BENCHMARK_LIB" run_kimi_vendor_eval() { return 7; } @@ -162,7 +160,7 @@ def test_kimi_failure_preserves_rc_without_eval_only() -> None: export IS_AGENTIC=0 unset EVAL_ONLY run_eval --port 8888 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -175,14 +173,10 @@ def test_kimi_failure_preserves_rc_without_eval_only() -> None: assert "unbound variable" not in result.stderr - - def test_recipe_lm_eval_arg_still_lm_eval_on_fixed_seqlen(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="0", cli_fw="lm-eval") - - def _run_invalid_call(call: str) -> subprocess.CompletedProcess: env = { **os.environ, @@ -213,16 +207,14 @@ def test_run_eval_rejects_unsafe_suite_name() -> None: def test_run_eval_rejects_suite_override_for_lm_eval() -> None: - result = _run_invalid_call( - "EVAL_SUITE=gpqa_diamond run_eval --framework lm-eval" - ) + result = _run_invalid_call("EVAL_SUITE=gpqa_diamond run_eval --framework lm-eval") assert result.returncode == 2 assert "only supported with kimi-vendor, minimax-vendor, or bfcl" in result.stderr def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" run_kimi_vendor_eval() { export EVAL_SUITE=kimi_tool_call_schema @@ -248,7 +240,7 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: printf 'LM_COMPLETED=%s\n' "${EVAL_COMPLETED_SUITE:-unset}" append_lm_eval_summary printf 'FINAL_SUITE=%s\n' "${EVAL_SUITE-unset}" -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -268,7 +260,7 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: def test_kimi_default_suite_reaches_eval_only_metadata() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" run_kimi_vendor_eval() { echo "DISPATCH=$EVAL_SUITE"; } append_lm_eval_summary() { echo "METADATA=$EVAL_COMPLETED_SUITE"; } @@ -278,7 +270,7 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: export EVAL_CONCURRENT_REQUESTS="" unset EVAL_SUITE run_eval --port 8888 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -293,7 +285,7 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: def test_agentic_eval_propagates_artifact_staging_failure() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" run_kimi_vendor_eval() { :; } append_lm_eval_summary() { return 73; } @@ -303,7 +295,7 @@ def test_agentic_eval_propagates_artifact_staging_failure() -> None: export EVAL_CONCURRENT_REQUESTS="" unset EVAL_SUITE run_eval --port 8888 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -315,14 +307,15 @@ def test_agentic_eval_propagates_artifact_staging_failure() -> None: assert result.returncode == 73 assert "eval artifact staging failed with exit code 73" in result.stderr + def test_kimi_full_suite_dispatches_to_schema_runner() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" _run_kimi_tool_call_schema_eval() { printf 'DISPATCH=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" } EVAL_SUITE=kimi_tool_call_schema_full run_kimi_vendor_eval --port 9999 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -336,13 +329,13 @@ def test_kimi_full_suite_dispatches_to_schema_runner() -> None: def test_minimax_full_suite_dispatches_to_full_runner() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" _run_minimax_m3_full_eval() { printf 'DISPATCH=%s ARGS=<%s>\n' "$EVAL_SUITE" "$*" } EVAL_SUITE=minimax_m3_full run_minimax_vendor_eval --port 9999 -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -366,15 +359,13 @@ def test_kimi_vendor_rejects_batched_concurrency() -> None: def test_kimi_vendor_rejects_unsupported_suite() -> None: - result = _run_invalid_call( - "EVAL_SUITE=gsm8k run_kimi_vendor_eval" - ) + result = _run_invalid_call("EVAL_SUITE=gsm8k run_kimi_vendor_eval") assert result.returncode == 2 assert "unsupported Kimi Vendor Verifier suite 'gsm8k'" in result.stderr def _run_minimax_dispatch(*, suite: str | None = None, concurrency: str = "") -> str: - script = r''' + script = r""" source "$BENCHMARK_LIB" unset EVAL_MAX_MODEL_LEN compute_eval_context_length() { echo "UNEXPECTED_CONTEXT_LOAD"; return 99; } @@ -389,7 +380,7 @@ def _run_minimax_dispatch(*, suite: str | None = None, concurrency: str = "") -> run_eval --framework minimax-vendor --port 9999 printf 'DISPATCH_COUNT=%s\n' "$MINIMAX_DISPATCH_COUNT" printf 'COMPLETED_SUITE=%s\n' "$EVAL_COMPLETED_SUITE" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -431,9 +422,7 @@ def test_minimax_vendor_accepts_explicit_supported_suite() -> None: def test_minimax_vendor_rejects_unsupported_suite() -> None: result = _run_invalid_call( - "MODEL_PREFIX=minimaxm3 " - "EVAL_SUITE=gsm8k " - "run_eval --framework minimax-vendor" + "MODEL_PREFIX=minimaxm3 EVAL_SUITE=gsm8k run_eval --framework minimax-vendor" ) assert result.returncode == 2 @@ -468,12 +457,12 @@ def test_minimax_vendor_ignores_single_launcher_concurrency_value() -> None: def test_minimax_vendor_accepts_non_m3_model() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" _run_minimax_m3_smoke_eval() { echo "DISPATCH=$EVAL_SUITE"; } unset EVAL_SUITE EVAL_RESULT_DIR MODEL=moonshotai/Kimi-K3 MODEL_PREFIX=kimik3 run_minimax_vendor_eval -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -486,12 +475,12 @@ def test_minimax_vendor_accepts_non_m3_model() -> None: def test_minimax_vendor_accepts_case_insensitive_m3_model_name() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" _run_minimax_m3_smoke_eval() { echo "DISPATCH=$EVAL_SUITE"; } unset MODEL_PREFIX EVAL_SUITE EVAL_RESULT_DIR MODEL_NAME=vendor/MINIMAX-M3-custom run_minimax_vendor_eval -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -506,7 +495,7 @@ def test_minimax_vendor_accepts_case_insensitive_m3_model_name() -> None: def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( tmp_path: Path, ) -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" unset EVAL_SUITE EVAL_RESULT_DIR EVAL_COMPLETED_SUITE unset VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR @@ -525,7 +514,7 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( export VENDOR_VERIFIER_PYTHON="$PYTHON_DIR/python3" export VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" } -_prepare_minimax_vendor_runtime() { return 12; } +_prepare_minimax_m3_full_runtime() { return 12; } append_lm_eval_summary() { printf 'STAGED=<%s>\n' "$EVAL_RESULT_DIR" printf 'STAGED_CONC=<%s>\n' "$CONC" @@ -538,7 +527,7 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( run_eval --framework minimax-vendor --results-dir "$RESULTS_DIR" eval_rc=$? printf 'EVAL_RC=%s\n' "$eval_rc" -''' +""" results_dir = tmp_path / "results" results_dir.mkdir() (results_dir / "results_minimax_vendor_stale.json").write_text("{}") @@ -560,12 +549,15 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( assert "EVAL_RC=12" in output assert "STALE_MINIMAX_ARTIFACT" not in output assert not (tmp_path / "python").exists() - assert f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/minimax_provider_eval.py'}>" in output + assert ( + f"ADAPTER_ARG=<{REPO_ROOT / 'utils/evals/minimax_provider_eval.py'}>" in output + ) assert "ADAPTER_ARG=" in output assert f"ADAPTER_ARG=<{results_dir}>" in output - assert "ADAPTER_ARG=<--integration-error>" in output + assert "ADAPTER_ARG=" in output + assert "ADAPTER_ARG=<--message>" in output assert ( - "ADAPTER_ARG=" ) in output assert f"STAGED=<{results_dir}>" in output @@ -573,43 +565,15 @@ def test_minimax_vendor_setup_failure_uses_integration_error_and_stages( assert "STAGED_CONC=<7>" in output -def test_minimax_vendor_dependency_install_is_pinned_and_minimal( - tmp_path: Path, -) -> None: - script = r''' -source "$BENCHMARK_LIB" -selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } -VENDOR_VERIFIER_PYTHON=selected_python -_install_minimax_vendor_eval_deps "$RUNTIME_DIR" -''' - result = subprocess.run( - ["bash", "-c", script], - env={ - **os.environ, - "BENCHMARK_LIB": str(BENCHMARK_LIB), - "RUNTIME_DIR": str(tmp_path / "runtime"), - }, - text=True, - capture_output=True, - check=True, - ) - - assert "PYTHON_ARG=" in result.stdout - assert "PYTHON_ARG= None: - script = r''' + script = r""" source "$BENCHMARK_LIB" selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } VENDOR_VERIFIER_PYTHON=selected_python _install_minimax_m3_full_deps "$RUNTIME_DIR" -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -638,7 +602,7 @@ def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> No results_dir = tmp_path / "results" runtime_dir = tmp_path / "runtime" python_dir = tmp_path / "python" - script = r''' + script = r""" source "$BENCHMARK_LIB" selected_python() { printf 'PYTHONPATH=<%s>\n' "$PYTHONPATH" >&2 @@ -650,15 +614,15 @@ def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> No VENDOR_VERIFIER_PYTHON_CLEANUP_DIR="$PYTHON_DIR" export VENDOR_VERIFIER_PYTHON VENDOR_VERIFIER_PYTHON_CLEANUP_DIR } -_prepare_minimax_vendor_runtime() { - mkdir "$RUNTIME_DIR" +_prepare_minimax_m3_full_runtime() { + mkdir -p "$RUNTIME_DIR/source" "$RUNTIME_DIR/deps" printf '%s\n' "$RUNTIME_DIR" } mktemp() { echo "UNEXPECTED_DEFAULT_RESULTS_DIR" >&2; return 99; } run_minimax_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -682,26 +646,26 @@ def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> No adapter = REPO_ROOT / "utils/evals/minimax_provider_eval.py" fixture = REPO_ROOT / "utils/evals/minimax_m3_smoke.json" - assert f"PYTHONPATH=<{runtime_dir}" in output for value in ( adapter, + "run", + "selected_python", + runtime_dir / "source", + runtime_dir / "deps", "http://127.0.0.1:9999/v1", - "EMPTY", "test-model", results_dir, fixture, - "180", - "900", ): assert f"PYTHON_ARG=<{value}>" in output for option in ( + "--python", + "--source-dir", + "--dependency-dir", "--base-url", - "--api-key", "--model", "--output-dir", "--fixture", - "--request-timeout-seconds", - "--timeout-seconds", ): assert f"PYTHON_ARG=<{option}>" in output assert "must-not-be-forwarded" not in output @@ -712,7 +676,6 @@ def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> No assert not python_dir.exists() - def test_kimi_vendor_setup_failure_writes_compatibility_result( tmp_path: Path, ) -> None: @@ -721,7 +684,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( (results_dir / "results_kimi_vendor_stale.json").write_text("{}") (results_dir / "kimi_vendor_report.json").write_text("{}") python_dir = tmp_path / "python" - script = r''' + script = r""" source "$BENCHMARK_LIB" _prepare_vendor_verifier_python() { if compgen -G "$RESULTS_DIR/results_kimi_vendor_*.json" >/dev/null \ @@ -742,7 +705,7 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( _prepare_kimi_vendor_runtime() { return 12; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'SETUP_RC=%s\n' "$?" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -776,19 +739,16 @@ def test_kimi_vendor_setup_failure_writes_compatibility_result( assert len(score_files) == 1 score_result = json.loads(score_files[0].read_text()) assert ( - score_result["results"]["kimi_tool_call_schema"][ - "exact_match,strict-match" - ] + score_result["results"]["kimi_tool_call_schema"]["exact_match,strict-match"] == 0.0 ) assert score_result["integration_error"]["message"] == message - native_result = json.loads( - (results_dir / "kimi_vendor_report.json").read_text() - ) + native_result = json.loads((results_dir / "kimi_vendor_report.json").read_text()) assert native_result["completed"] is False assert native_result["integration_error"]["message"] == message assert not python_dir.exists() + def test_preclear_failure_cannot_stage_stale_provider_result(tmp_path: Path) -> None: results_dir = tmp_path / "results" results_dir.mkdir() @@ -796,7 +756,7 @@ def test_preclear_failure_cannot_stage_stale_provider_result(tmp_path: Path) -> stale_result.write_text('{"stale": true}\n') work_dir = tmp_path / "work" work_dir.mkdir() - script = r''' + script = r""" source "$BENCHMARK_LIB" rm() { return 73; } append_lm_eval_summary() { @@ -811,7 +771,7 @@ def test_preclear_failure_cannot_stage_stale_provider_result(tmp_path: Path) -> run_eval --framework kimi-vendor --results-dir "$RESULTS_DIR" printf 'EVAL_RC=%s\n' "$?" printf 'EVAL_RESULT_DIR=<%s>\n' "${EVAL_RESULT_DIR:-}" -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -930,14 +890,15 @@ def _prepare_local_kimi_verifier( payload: bytes, verifier_ref: str = "1" * 40, transient_failures: int = 0, + archive_sha256: str | None = None, ) -> tuple[subprocess.CompletedProcess[str], Path, list[str]]: checkout = tmp_path / "checkout" - script = r''' + script = r""" source "$BENCHMARK_LIB" git() { echo "git must not be invoked" >&2; return 127; } mktemp() { mkdir "$CHECKOUT"; printf '%s\n' "$CHECKOUT"; } -_prepare_kimi_vendor_verifier "$REPO_URL" "$VERIFIER_REF" -''' +_prepare_kimi_vendor_verifier "$REPO_URL" "$VERIFIER_REF" "$ARCHIVE_SHA256" +""" with _serve_archive( payload, transient_failures=transient_failures, @@ -950,6 +911,7 @@ def _prepare_local_kimi_verifier( "CHECKOUT": str(checkout), "REPO_URL": repo_url, "VERIFIER_REF": verifier_ref, + "ARCHIVE_SHA256": archive_sha256 or hashlib.sha256(payload).hexdigest(), }, text=True, capture_output=True, @@ -957,7 +919,9 @@ def _prepare_local_kimi_verifier( return result, checkout, request_paths -def test_kimi_vendor_verifier_fetches_expected_subset_without_git(tmp_path: Path) -> None: +def test_kimi_vendor_verifier_fetches_expected_subset_without_git( + tmp_path: Path, +) -> None: result, checkout, request_paths = _prepare_local_kimi_verifier( tmp_path, _kimi_verifier_archive(), @@ -996,6 +960,20 @@ def test_kimi_vendor_verifier_retries_transient_archive_failure( assert "archive download attempt 1/3 failed" in result.stderr +def test_kimi_vendor_verifier_rejects_archive_hash_mismatch( + tmp_path: Path, +) -> None: + result, checkout, _ = _prepare_local_kimi_verifier( + tmp_path, + _kimi_verifier_archive(), + archive_sha256="0" * 64, + ) + + assert result.returncode == 1 + assert "archive SHA256 mismatch" in result.stderr + assert not checkout.exists() + + def test_kimi_vendor_verifier_removes_partial_checkout_when_member_missing( tmp_path: Path, ) -> None: @@ -1025,7 +1003,7 @@ def test_kimi_vendor_verifier_rejects_unsafe_archive_members(tmp_path: Path) -> def test_kimi_vendor_uses_system_python_fast_path() -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" python3() { printf 'SYSTEM_PYTHON_ARG=<%s>\n' "$@" @@ -1037,7 +1015,7 @@ def test_kimi_vendor_uses_system_python_fast_path() -> None: _prepare_vendor_verifier_python "Kimi Vendor Verifier" "kimi-vendor-python" printf 'SELECTED_PYTHON=<%s>\n' "$VENDOR_VERIFIER_PYTHON" printf 'PYTHON_CLEANUP=<%s>\n' "$VENDOR_VERIFIER_PYTHON_CLEANUP_DIR" -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -1058,7 +1036,7 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( log_path = tmp_path / "bootstrap.log" fake_uv = tmp_path / "fake-uv" fake_uv.write_text( - r'''#!/usr/bin/env bash + r"""#!/usr/bin/env bash printf 'UV_CACHE_DIR=<%s>\n' "$UV_CACHE_DIR" >> "$KIMI_LOG" printf 'UV_PYTHON_INSTALL_DIR=<%s>\n' "$UV_PYTHON_INSTALL_DIR" >> "$KIMI_LOG" printf 'UV_ARG=<%s>\n' "$@" >> "$KIMI_LOG" @@ -1069,10 +1047,10 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( printf 'SELECTED_PYTHON_ARG=<%s>\n' "$@" >> "$KIMI_LOG" PYTHON chmod +x "$venv_dir/bin/python" -''' +""" ) fake_uv.chmod(0o755) - script = r''' + script = r""" source "$BENCHMARK_LIB" python3() { if [ "$1" = "-c" ]; then @@ -1101,7 +1079,7 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( _install_kimi_vendor_eval_deps "$runtime_dir" _cleanup_vendor_eval "$runtime_dir" "$cleanup_dir" [ ! -e "$runtime_dir" ] && [ ! -e "$cleanup_dir" ] && printf 'CLEANED\n' -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -1128,7 +1106,7 @@ def test_kimi_vendor_bootstraps_pinned_python_and_cleans_it( assert "UV_CACHE_DIR=" in log - assert "SELECTED_PYTHON_ARG=" in log + assert "SELECTED_PYTHON_ARG= None: runtime_dir = tmp_path / "runtime" - script = r''' + script = r""" source "$BENCHMARK_LIB" selected_python() { printf 'PYTHON_ARG=<%s>\n' "$@"; } VENDOR_VERIFIER_PYTHON=selected_python _install_kimi_vendor_eval_deps "$RUNTIME_DIR" -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -1156,19 +1134,19 @@ def test_kimi_vendor_dependency_install_is_isolated(tmp_path: Path) -> None: assert "PYTHON_ARG=<--target>" in result.stdout assert f"PYTHON_ARG=<{runtime_dir}>" in result.stdout - assert "PYTHON_ARG=" in result.stdout + assert "PYTHON_ARG= None: - script = r''' + script = r""" source "$BENCHMARK_LIB" _prepare_vendor_verifier_python() { return 12; } _write_kimi_vendor_integration_error() { return 23; } run_kimi_vendor_eval --results-dir "$RESULTS_DIR" printf 'EVAL_RC=%s\n' "$?" -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -1187,8 +1165,6 @@ def test_kimi_vendor_surfaces_failure_artifact_error(tmp_path: Path) -> None: assert "failed to write Kimi verifier failure artifact" in result.stderr - - def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( tmp_path: Path, ) -> None: @@ -1197,7 +1173,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( runtime_dir = tmp_path / "runtime" python_dir = tmp_path / "python" verifier_dir.mkdir() - script = r''' + script = r""" source "$BENCHMARK_LIB" _prepare_vendor_verifier_python() { mkdir "$PYTHON_DIR" @@ -1212,7 +1188,8 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( } _prepare_kimi_vendor_verifier() { printf 'CHECKOUT=%s@%s\n' "$1" "$2" >&2 - "$VENDOR_VERIFIER_PYTHON" - "$1" "$2" "$VERIFIER_DIR" <<'PY' >&2 + printf 'CHECKOUT_SHA=%s\n' "$3" >&2 + "$VENDOR_VERIFIER_PYTHON" - "$1" "$2" "$3" "$VERIFIER_DIR" <<'PY' >&2 archive extraction PY printf '%s\n' "$VERIFIER_DIR" @@ -1225,7 +1202,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( run_kimi_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1261,10 +1238,12 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( "CHECKOUT=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" "@b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" ) in output - assert "PYTHON_ARG=<->" in output assert ( - "PYTHON_ARG=" in output + "CHECKOUT_SHA=ab933117c894a785978f8aee0f052e5a9096b3029e7962354b1c07ea430588c3" + in output ) + assert "PYTHON_ARG=<->" in output + assert "PYTHON_ARG=" in output for value in ( adapter, verifier_dir, @@ -1288,6 +1267,7 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( assert not verifier_dir.exists() assert not python_dir.exists() + def test_kimi_full_runner_installs_xdist_sets_timeout_and_cleans_runtimes( tmp_path: Path, ) -> None: @@ -1295,7 +1275,7 @@ def test_kimi_full_runner_installs_xdist_sets_timeout_and_cleans_runtimes( verifier_dir = tmp_path / "verifier" runtime_dir = tmp_path / "runtime" python_dir = tmp_path / "python" - script = r''' + script = r""" source "$BENCHMARK_LIB" _prepare_vendor_verifier_python() { mkdir "$PYTHON_DIR" @@ -1320,7 +1300,7 @@ def test_kimi_full_runner_installs_xdist_sets_timeout_and_cleans_runtimes( run_kimi_vendor_eval --port 9999 --results-dir "$RESULTS_DIR" printf 'EVAL_SUITE=%s\n' "$EVAL_SUITE" printf 'EVAL_RESULT_DIR=%s\n' "$EVAL_RESULT_DIR" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1363,13 +1343,13 @@ def test_run_lm_eval_rejects_missing_option_value(): def test_lm_patch_copy_resolves_outside_repo(tmp_path): - script = r''' + script = r""" source "$BENCHMARK_LIB" cd "$OTHER_CWD" _patch_lm_eval patch_dir=${PYTHONPATH%%:*} cmp "$(_eval_patches_dir)/lm_eval_sitecustomize.py" "$patch_dir/sitecustomize.py" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1379,8 +1359,7 @@ def test_lm_patch_copy_resolves_outside_repo(tmp_path): subprocess.run(["bash", "-c", script], env=env, check=True) - -_EVAL_LIMIT_SCRIPT = r''' +_EVAL_LIMIT_SCRIPT = r""" set -e SHIM_DIR=$(mktemp -d) cat > "$SHIM_DIR/python3" <<'PY' @@ -1401,7 +1380,7 @@ def test_lm_patch_copy_resolves_outside_repo(tmp_path): _patch_lm_eval() { :; } PATH="$SHIM_DIR:$PATH" run_lm_eval --port 9999 2>&1 -''' +""" def _run_lm_eval_cmdline(*, eval_limit=None) -> str: @@ -1443,11 +1422,11 @@ def _summary_metadata(tmp_path: Path, **overrides: str) -> dict: results_dir = tmp_path / "results" work_dir.mkdir(parents=True) results_dir.mkdir() - script = r''' + script = r""" source "$BENCHMARK_LIB" cd "$WORK_DIR" append_lm_eval_summary >/dev/null -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1471,11 +1450,11 @@ def test_summary_stages_bfcl_upstream_archive_before_cleanup(tmp_path: Path) -> results_dir.mkdir() archive = results_dir / "bfcl_upstream_artifacts.tar.gz" archive.write_bytes(b"bfcl-archive") - script = r''' + script = r""" source "$BENCHMARK_LIB" cd "$WORK_DIR" append_lm_eval_summary >/dev/null -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1515,10 +1494,10 @@ def test_stage_eval_artifacts_copies_eval_outputs_only(tmp_path: Path) -> None: source = source_one if filename.endswith(".json") else source_two (source / filename).write_text(filename) (source_one / "unrelated.log").write_text("skip") - script = r''' + script = r""" source "$BENCHMARK_LIB" stage_eval_artifacts "$DESTINATION" "$SOURCE_ONE" "$SOURCE_TWO" -''' +""" subprocess.run( ["bash", "-c", script], env={ @@ -1539,11 +1518,11 @@ def test_stage_eval_artifacts_propagates_copy_failure(tmp_path: Path) -> None: source = tmp_path / "source" source.mkdir() (source / "bfcl_report.json").write_text("{}") - script = r''' + script = r""" source "$BENCHMARK_LIB" cp() { return 73; } stage_eval_artifacts "$DESTINATION" "$SOURCE" -''' +""" result = subprocess.run( ["bash", "-c", script], @@ -1559,13 +1538,14 @@ def test_stage_eval_artifacts_propagates_copy_failure(tmp_path: Path) -> None: assert result.returncode == 73 + def test_stage_eval_artifacts_fails_when_no_artifacts_exist(tmp_path: Path) -> None: source = tmp_path / "source" source.mkdir() - script = r''' + script = r""" source "$BENCHMARK_LIB" stage_eval_artifacts "$DESTINATION" "$SOURCE" -''' +""" result = subprocess.run( ["bash", "-c", script], @@ -1591,12 +1571,12 @@ def test_summary_propagates_artifact_staging_failure(tmp_path: Path) -> None: work_dir.mkdir() results_dir.mkdir() (results_dir / "results_eval.json").write_text("{}") - script = r''' + script = r""" source "$BENCHMARK_LIB" cp() { return 73; } cd "$WORK_DIR" append_lm_eval_summary -''' +""" result = subprocess.run( ["bash", "-c", script], env={ @@ -1632,15 +1612,16 @@ def test_summary_metadata_preserves_single_node_expert_parallelism( assert meta["prefill_ep"] == 8 assert meta["decode_ep"] == 8 + def test_run_lm_eval_exports_cli_task_path(tmp_path: Path) -> None: - script = r''' + script = r""" source "$BENCHMARK_LIB" python3() { :; } export EVAL_MAX_MODEL_LEN=16384 export INFERENCEX_LM_EVAL_RUNTIME_READY=true run_lm_eval --task custom.yaml --results-dir "$RESULTS_DIR" printf 'EVAL_TASKS_DIR=%s\n' "$EVAL_TASKS_DIR" -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1690,7 +1671,7 @@ def test_summary_metadata_prefers_completed_eval_identity(tmp_path: Path) -> Non def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: - script = r''' + script = r""" set -u source "$BENCHMARK_LIB" for value in TrUe yEs oN 1 false 0; do @@ -1712,7 +1693,7 @@ def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: echo false fi done -''' +""" result = subprocess.run( ["bash", "-c", script], env={**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB)}, @@ -1733,8 +1714,7 @@ def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: ] - -_MODAL_CREDS_SCRIPT = r''' +_MODAL_CREDS_SCRIPT = r""" source "$BENCHMARK_LIB" _ensure_modal_credentials echo "HOME_AFTER=$HOME" @@ -1743,10 +1723,12 @@ def test_env_is_true_is_case_insensitive_and_unset_safe() -> None: PERMS=$(stat -c '%a' "$HOME/.modal.toml" 2>/dev/null || stat -f '%A' "$HOME/.modal.toml" 2>/dev/null) echo "TOML_PERMS=$PERMS" fi -''' +""" -def _run_modal_creds(tmp_path: Path, *, home: str, token_id="tok-id", token_secret="tok-secret") -> str: +def _run_modal_creds( + tmp_path: Path, *, home: str, token_id="tok-id", token_secret="tok-secret" +) -> str: env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1784,7 +1766,9 @@ def test_modal_creds_remaps_home_when_not_writable_parent(tmp_path): nested_home = str(readonly_parent / "nested_home") try: out = _run_modal_creds(tmp_path, home=nested_home) - assert "HOME_AFTER=/tmp/inferencex-modal-home" in out, f"Expected HOME remap:\n{out}" + assert "HOME_AFTER=/tmp/inferencex-modal-home" in out, ( + f"Expected HOME remap:\n{out}" + ) assert "remapped" in out.lower() or "HOME remapped" in out assert "TOML_EXISTS=true" in out toml_path = Path("/tmp/inferencex-modal-home/.modal.toml") @@ -1800,7 +1784,9 @@ def test_modal_creds_remaps_home_when_not_writable(tmp_path): readonly_home.mkdir(mode=0o555) try: out = _run_modal_creds(tmp_path, home=str(readonly_home)) - assert "HOME_AFTER=/tmp/inferencex-modal-home" in out, f"Expected HOME remap:\n{out}" + assert "HOME_AFTER=/tmp/inferencex-modal-home" in out, ( + f"Expected HOME remap:\n{out}" + ) assert "TOML_EXISTS=true" in out finally: readonly_home.chmod(0o755) @@ -1828,8 +1814,7 @@ def test_modal_creds_no_remap_when_disabled(tmp_path): assert "TOML_EXISTS" not in out - -_INCLUDE_PATH_SCRIPT = r''' +_INCLUDE_PATH_SCRIPT = r""" set -e SHIM_DIR=$(mktemp -d) cat > "$SHIM_DIR/python3" <<'PY' @@ -1850,7 +1835,7 @@ def test_modal_creds_no_remap_when_disabled(tmp_path): _patch_lm_eval() { :; } PATH="$SHIM_DIR:$PATH" run_lm_eval --port 9999 2>&1 -''' +""" def _run_lm_eval_with_include_path( @@ -1906,7 +1891,7 @@ def test_include_path_absent_when_eval_include_path_unset(): def test_swebench_single_shot_registers_task_yaml(): - script = r''' + script = r""" source "$BENCHMARK_LIB" run_lm_eval() { echo "TASK=$EVAL_TASKS_DIR" @@ -1917,7 +1902,7 @@ def test_swebench_single_shot_registers_task_yaml(): export EVAL_TASKS_DIR="$TASK_YAML" export MODEL=test-model run_swebench_eval -''' +""" env = { **os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), @@ -1949,7 +1934,9 @@ def test_modal_credentials_sanitizes_whitespace_contaminated_tokens(tmp_path): echo SANITIZED_OK """ env = {**os.environ, "BENCHMARK_LIB": str(BENCHMARK_LIB), "HOME": str(home)} - res = subprocess.run(["bash", "-c", script], env=env, text=True, capture_output=True) + res = subprocess.run( + ["bash", "-c", script], env=env, text=True, capture_output=True + ) assert res.returncode == 0, res.stdout + res.stderr assert "SANITIZED_OK" in res.stdout @@ -1963,7 +1950,7 @@ def test_agentic_generation_invokes_mini_swe_agent(tmp_path): 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_name_or_path\": \"m\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_name_or_path": "m", "model_patch": "d"}}\' > "$out/preds.json"\n' ) (shim / "mini-extra").chmod(0o755) default_yaml = shim / "default.yaml" @@ -1990,11 +1977,15 @@ def test_agentic_generation_invokes_mini_swe_agent(tmp_path): grep -q 'runtime_timeout: 3600' "$GEN_DIR/mini_swebench_overrides.yaml" || { echo NO_RUNTIME_TIMEOUT; exit 1; } echo AGENTIC_GEN_OK """ - env = {**os.environ, - "BENCHMARK_LIB": str(BENCHMARK_LIB), - "GEN_DIR": str(gen_dir), - "PATH": f"{shim}:{os.environ['PATH']}"} - res = subprocess.run(["bash", "-c", script], env=env, text=True, capture_output=True) + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "GEN_DIR": str(gen_dir), + "PATH": f"{shim}:{os.environ['PATH']}", + } + res = subprocess.run( + ["bash", "-c", script], env=env, text=True, capture_output=True + ) assert res.returncode == 0, res.stdout + res.stderr assert "AGENTIC_GEN_OK" in res.stdout argv = (shim / "argv.log").read_text() @@ -2028,37 +2019,45 @@ def _run_agentic(shim, gen_dir, extra_env=None): _run_swebench_agentic_generation "$GEN_DIR" --port 8899 echo "GEN_RC=$?" """ - env = {**os.environ, - "BENCHMARK_LIB": str(BENCHMARK_LIB), - "GEN_DIR": str(gen_dir), - "MODEL_NAME": "test-model", - "SWEBENCH_SANDBOX_SWEEP": "0", - "SWEBENCH_WATCHDOG_POLL": "1", - "PATH": f"{shim}:{os.environ['PATH']}", - **(extra_env or {})} - return subprocess.run(["bash", "-c", script], env=env, text=True, capture_output=True) + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "GEN_DIR": str(gen_dir), + "MODEL_NAME": "test-model", + "SWEBENCH_SANDBOX_SWEEP": "0", + "SWEBENCH_WATCHDOG_POLL": "1", + "PATH": f"{shim}:{os.environ['PATH']}", + **(extra_env or {}), + } + return subprocess.run( + ["bash", "-c", script], env=env, text=True, capture_output=True + ) def test_agentic_watchdog_kills_hung_mini(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, + shim, gen_dir = _agentic_shim( + tmp_path, 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" - "exec sleep 600 /dev/null 2>&1\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n' + "exec sleep 600 /dev/null 2>&1\n", + ) + res = _run_agentic( + shim, gen_dir, {"EVAL_LIMIT": "1", "SWEBENCH_AGENT_EXIT_GRACE": "2"} ) - res = _run_agentic(shim, gen_dir, {"EVAL_LIMIT": "1", "SWEBENCH_AGENT_EXIT_GRACE": "2"}) assert "GEN_RC=0" in res.stdout, res.stdout + res.stderr assert "hung after completing all instances" in res.stdout + res.stderr def test_agentic_salvage_partial_preds_on_failure(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, + shim, gen_dir = _agentic_shim( + tmp_path, 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" - "exit 7\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n' + "exit 7\n", ) res = _run_agentic(shim, gen_dir, {"EVAL_LIMIT": "2"}) assert "GEN_RC=0" in res.stdout, res.stdout + res.stderr @@ -2072,12 +2071,13 @@ def test_agentic_no_preds_still_fails(tmp_path): def test_agentic_eval_limit_defaults_to_full_split(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, - 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + '\n' + shim, gen_dir = _agentic_shim( + tmp_path, + 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + "\n" 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n', ) body = (shim / "mini-extra").read_text().replace("ARGVLOG", str(shim / "argv.log")) (shim / "mini-extra").write_text(body) @@ -2088,12 +2088,13 @@ def test_agentic_eval_limit_defaults_to_full_split(tmp_path): def test_agentic_eval_limit_full_runs_whole_split(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, - 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + '\n' + shim, gen_dir = _agentic_shim( + tmp_path, + 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + "\n" 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n', ) body = (shim / "mini-extra").read_text().replace("ARGVLOG", str(shim / "argv.log")) (shim / "mini-extra").write_text(body) @@ -2314,8 +2315,7 @@ def test_single_node_eval_artifact_name_includes_suite_identity() -> None: assert "github.run_attempt" in expression - -_GENMODE_SCRIPT = r''' +_GENMODE_SCRIPT = r""" source "$BENCHMARK_LIB" 2>/dev/null _install_swebench_agent_deps() { :; } _ensure_modal_credentials() { :; } @@ -2331,7 +2331,7 @@ def test_single_node_eval_artifact_name_includes_suite_identity() -> None: } run_swebench_eval --port 8888 echo "RC=$?" -''' +""" def _gen_mode( @@ -2341,11 +2341,13 @@ def _gen_mode( gen_mode=None, eval_suite=None, ) -> str: - env = {**os.environ, - "BENCHMARK_LIB": str(BENCHMARK_LIB), - "KV_OFFLOADING": "none", - "IS_AGENTIC": is_agentic, - "EVAL_RESULT_DIR": str(tmp_path / "out")} + env = { + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "KV_OFFLOADING": "none", + "IS_AGENTIC": is_agentic, + "EVAL_RESULT_DIR": str(tmp_path / "out"), + } env.pop("SWEBENCH_GEN_MODE", None) env.pop("SCENARIO_TYPE", None) env.pop("EVAL_SUITE", None) @@ -2353,9 +2355,13 @@ def _gen_mode( env["SWEBENCH_GEN_MODE"] = gen_mode if eval_suite is not None: env["EVAL_SUITE"] = eval_suite - res = subprocess.run(["bash", "-c", _GENMODE_SCRIPT], env=env, - text=True, capture_output=True, - cwd=BENCHMARK_LIB.parents[1]) + res = subprocess.run( + ["bash", "-c", _GENMODE_SCRIPT], + env=env, + text=True, + capture_output=True, + cwd=BENCHMARK_LIB.parents[1], + ) assert "RC=42" in res.stdout, res.stdout + res.stderr return res.stdout @@ -2388,13 +2394,16 @@ def test_swebench_generation_modes_preserve_explicit_suite(tmp_path): def test_agent_sandbox_cpu_knob(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, + shim, gen_dir = _agentic_shim( + tmp_path, 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n', + ) + res = _run_agentic( + shim, gen_dir, {"EVAL_LIMIT": "1", "SWEBENCH_AGENT_SANDBOX_CPU": "1"} ) - res = _run_agentic(shim, gen_dir, {"EVAL_LIMIT": "1", "SWEBENCH_AGENT_SANDBOX_CPU": "1"}) assert "GEN_RC=0" in res.stdout, res.stdout + res.stderr cfg = (gen_dir / "mini_swebench_overrides.yaml").read_text() assert "modal_sandbox_kwargs" in cfg and "cpu: 1" in cfg, cfg @@ -2408,27 +2417,31 @@ def test_agent_sandbox_cpu_knob(tmp_path): def test_eval_limit_rejects_non_positive_integer(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, + shim, gen_dir = _agentic_shim( + tmp_path, 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n', ) for bad in ("-5", "abc", "3.5"): - gd = tmp_path / f"gen_{bad.replace('-','neg').replace('.','_')}" + gd = tmp_path / f"gen_{bad.replace('-', 'neg').replace('.', '_')}" gd.mkdir() res = _run_agentic(shim, gd, {"EVAL_LIMIT": bad}) - assert "GEN_RC=1" in res.stdout, f"EVAL_LIMIT={bad!r} should fail: {res.stdout}{res.stderr}" + assert "GEN_RC=1" in res.stdout, ( + f"EVAL_LIMIT={bad!r} should fail: {res.stdout}{res.stderr}" + ) assert "must be a positive integer" in res.stdout + res.stderr def test_eval_limit_full_and_zero_accepted(tmp_path): - shim, gen_dir = _agentic_shim(tmp_path, - 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + '\n' + shim, gen_dir = _agentic_shim( + tmp_path, + 'echo "MINI_ARGV: $*" >> ' + "ARGVLOG" + "\n" 'out=""; prev=""\n' 'for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n' 'mkdir -p "$out"\n' - "printf '{\"i1\": {\"instance_id\": \"i1\", \"model_patch\": \"d\"}}' > \"$out/preds.json\"\n" + 'printf \'{"i1": {"instance_id": "i1", "model_patch": "d"}}\' > "$out/preds.json"\n', ) body = (shim / "mini-extra").read_text().replace("ARGVLOG", str(shim / "argv.log")) (shim / "mini-extra").write_text(body) @@ -2436,7 +2449,9 @@ def test_eval_limit_full_and_zero_accepted(tmp_path): gd = tmp_path / f"gen_{sentinel}" gd.mkdir() res = _run_agentic(shim, gd, {"EVAL_LIMIT": sentinel}) - assert "GEN_RC=0" in res.stdout, f"EVAL_LIMIT={sentinel!r}: {res.stdout}{res.stderr}" + assert "GEN_RC=0" in res.stdout, ( + f"EVAL_LIMIT={sentinel!r}: {res.stdout}{res.stderr}" + ) argv = (shim / "argv.log").read_text() assert "--slice" not in argv @@ -2463,6 +2478,7 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: "toJson(matrix.config['kv-offload-backend']) || '' }}" ) + def test_fixed_eval_workflows_forward_provider_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) for job_name in ("test-sweep-evals", "test-sweep-multi-node-evals"): @@ -2480,7 +2496,6 @@ def test_fixed_eval_workflows_forward_provider_contract() -> None: assert "bfcl_vllm_kimi" in SINGLE_NODE_WORKFLOW.read_text() - def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) forwarded = workflow["jobs"]["test-sweep-multi-node-agentic-evals"]["with"] @@ -2506,9 +2521,7 @@ def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: if step.get("id") == "get-jobs" ) flatten_command = next( - line - for line in get_jobs["run"].splitlines() - if "rows.extend" in line + line for line in get_jobs["run"].splitlines() if "rows.extend" in line ) assert '"multinode_agentic_evals"' in flatten_command @@ -2523,12 +2536,14 @@ def test_env_can_force_bfcl_on_agentic_eval() -> None: assert "DISPATCH=bfcl" in output assert "STAGED=summary" in output + def test_cli_can_force_bfcl_on_fixed_seqlen_eval() -> None: output = _dispatch(is_agentic="0", cli_fw="bfcl") assert "DISPATCH=bfcl" in output assert "STAGED=summary" not in output + def test_bfcl_defaults_suite_dispatches_once_without_context_loading() -> None: script = r""" source "$BENCHMARK_LIB" @@ -2564,6 +2579,7 @@ def test_bfcl_defaults_suite_dispatches_once_without_context_loading() -> None: assert "STAGED=bfcl_smoke" not in result.stdout assert "UNEXPECTED_CONTEXT_LOAD" not in result.stdout + def test_bfcl_rejects_suite_from_another_provider() -> None: result = _run_invalid_call( "EVAL_CONCURRENT_REQUESTS='' " @@ -2574,6 +2590,7 @@ def test_bfcl_rejects_suite_from_another_provider() -> None: assert result.returncode == 2 assert "unsupported BFCL suite 'minimax_m3_smoke'" in result.stderr + def test_bfcl_suite_is_rejected_by_mismatched_framework() -> None: result = _run_invalid_call( "EVAL_CONCURRENT_REQUESTS='' " @@ -2584,12 +2601,14 @@ def test_bfcl_suite_is_rejected_by_mismatched_framework() -> None: assert result.returncode == 2 assert "unsupported MiniMax Provider Verifier suite 'bfcl_smoke'" in result.stderr + def test_bfcl_rejects_unknown_suite() -> None: result = _run_invalid_call("EVAL_SUITE=not_a_bfcl_suite run_bfcl_eval") assert result.returncode == 2 assert "unsupported BFCL suite 'not_a_bfcl_suite'" in result.stderr + def test_bfcl_full_suite_thresholds_are_diagnostic_and_namespaced() -> None: thresholds = yaml.safe_load( (REPO_ROOT / "utils/evals/thresholds.yaml").read_text() @@ -2687,6 +2706,7 @@ def test_bfcl_dependency_timeout_uses_integration_error_and_stages( assert not (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() assert not python_dir.exists() + def _run_bfcl_adapter_command( tmp_path: Path, *, @@ -2793,6 +2813,7 @@ def _run_bfcl_adapter_command( ) return result, (results_dir, runtime_dir, python_dir, project_root) + def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( tmp_path: Path, ) -> None: @@ -2815,8 +2836,6 @@ def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( str(project_root), "--num-threads", "4", - "--request-timeout-seconds", - "180", ): assert f"ADAPTER_ARG=<{value}>" in output assert "PREPARE_ARG=" in output @@ -2838,6 +2857,7 @@ def test_bfcl_runner_uses_fixed_adapter_contract_and_cleans_runtime( assert not python_dir.exists() assert not project_root.exists() + def test_bfcl_full_suites_use_suite_specific_runtime_and_archive_before_cleanup( tmp_path: Path, ) -> None: @@ -2865,8 +2885,7 @@ def test_bfcl_full_suites_use_suite_specific_runtime_and_archive_before_cleanup( assert f"ADAPTER_ARG=<{expected_threads}>" in output assert f"ARCHIVE_PROJECT_ROOT=<{project_root}>" in output assert ( - f"ARCHIVE_PATH=<{results_dir / 'bfcl_upstream_artifacts.tar.gz'}>" - in output + f"ARCHIVE_PATH=<{results_dir / 'bfcl_upstream_artifacts.tar.gz'}>" in output ) assert (results_dir / "bfcl_upstream_artifacts.tar.gz").exists() assert not runtime_dir.exists() @@ -2917,6 +2936,7 @@ def test_bfcl_adapter_timeout_writes_reports_stages_and_propagates( assert not python_dir.exists() assert not project_root.exists() + def test_bfcl_upstream_archive_is_deterministic_and_survives_cleanup( tmp_path: Path, ) -> None: @@ -3044,6 +3064,7 @@ def test_bfcl_installer_uses_verified_wheel_in_selected_venv(tmp_path: Path) -> assert "--break-system-packages" not in result.stdout assert "--target" not in result.stdout + def test_bfcl_python_preparation_exposes_system_site_packages( tmp_path: Path, ) -> None: From 80fde1cccc06e7156abdda07830ebb1a88d00027 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:24:48 -0500 Subject: [PATCH 64/99] feat: select exact generated deployments for eval shards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为评估分片精确选择生成的部署配置 --- utils/evals/EVALS.md | 5 + utils/matrix_logic/generate_sweep_configs.py | 456 +++-- .../test_generate_sweep_configs.py | 1823 +++++++++++------ 3 files changed, 1435 insertions(+), 849 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 0b22584b69..9ef230c419 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -77,6 +77,11 @@ uv run --no-project --with pydantic --with pyyaml --python 3.12 \ --evals-only --all-evals --trim-conc ``` +Capacity-limited campaigns can split a `test-config` result with `--conc` and +`--exp-names`. Each requested experiment name must match exactly one generated +row, so a shard cannot silently include another deployment that shares the same +configuration key and concurrency. + Run each generated matrix with the matching vendor smoke and `bfcl_smoke`. The full Kimi, MiniMax, and BFCL suites use the same endpoint and artifact paths, but are diagnostic model-quality campaigns rather than a replacement diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 938774aac9..3f8aa13874 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -21,10 +21,7 @@ validate_matrix_entry, ) -seq_len_stoi = { - "1k1k": (1024, 1024), - "8k1k": (8192, 1024) -} +seq_len_stoi = {"1k1k": (1024, 1024), "8k1k": (8192, 1024)} MIN_EVAL_CONC = 16 # Bound how many multinode agentic conc points share one server allocation. @@ -47,6 +44,7 @@ def seq_len_to_str(isl: int, osl: int) -> str: """ return seq_len_itos.get((isl, osl), f"{isl}_{osl}") + def freeze_config_value(value): """Convert JSON-shaped config values into deterministic hashable values.""" if isinstance(value, dict): @@ -109,9 +107,7 @@ def minimum_concurrency(entry: dict): kept_entry = {**kept_entry, "run-eval": True} if kept_entry.get("prefill") is not None: kept_entry["eval-conc"] = minimum_concurrency(kept_entry) - if any( - out[index].get("eval-all-concs") is True for index in indices - ): + if any(out[index].get("eval-all-concs") is True for index in indices): kept_entry["eval-all-concs"] = True out[keep] = kept_entry drop.update(index for index in indices if index != keep) @@ -183,9 +179,7 @@ def scheduling_gpus_per_node(label: str, runner_data: dict) -> int: if len(matches) == 1: return matches.pop() if not matches: - raise ValueError( - f"Cannot resolve {Fields.GPUS_PER_NODE.value} for '{label}'" - ) + raise ValueError(f"Cannot resolve {Fields.GPUS_PER_NODE.value} for '{label}'") raise ValueError( f"Ambiguous {Fields.GPUS_PER_NODE.value} for '{label}': {sorted(matches)}" ) @@ -271,10 +265,9 @@ def multinode_node_count( recipe_count = recipe_node_count(prefill, decode) if recipe_count is not None: return recipe_count - return ( - worker_node_count(prefill, "prefill", runner, runner_data) - + worker_node_count(decode, "decode", runner, runner_data) - ) + return worker_node_count( + prefill, "prefill", runner, runner_data + ) + worker_node_count(decode, "decode", runner, runner_data) def add_multinode_node_count( @@ -307,6 +300,7 @@ def effective_gpu_count(benchmark: dict) -> int: * benchmark.get(Fields.PCP_SIZE.value, 1) ) + def with_worker_parallelism_defaults(worker: dict) -> dict: """Return a worker config with explicit parallelism defaults.""" return { @@ -332,7 +326,8 @@ def multinode_worker_pair(benchmark: dict, disagg: bool) -> tuple[dict, dict]: **{ key: value for key, value in worker.items() - if key not in ( + if key + not in ( Fields.NUM_WORKER.value, Fields.ADDITIONAL_SETTINGS.value, ) @@ -403,11 +398,9 @@ def agentic_dram_offload_gb( gpus_per_node = runner_gpus_per_node(runner, runner_data) if Fields.WORKER.value in benchmark: - gpu_count = worker_gpus_per_node( - benchmark[Fields.WORKER.value], gpus_per_node) + gpu_count = worker_gpus_per_node(benchmark[Fields.WORKER.value], gpus_per_node) elif Fields.PREFILL.value in benchmark: - gpu_count = worker_gpus_per_node( - benchmark[Fields.PREFILL.value], gpus_per_node) + gpu_count = worker_gpus_per_node(benchmark[Fields.PREFILL.value], gpus_per_node) else: gpu_count = effective_gpu_count(benchmark) if gpu_count > gpus_per_node: @@ -419,8 +412,7 @@ def agentic_dram_offload_gb( f"{Fields.GPUS_PER_NODE.value}={gpus_per_node} for runner '{runner}'" ) proportional_bytes = ( - Decimal(available_mib) * BYTES_PER_MIB * utilization - * gpu_count / gpus_per_node + Decimal(available_mib) * BYTES_PER_MIB * utilization * gpu_count / gpus_per_node ) return int(proportional_bytes / BYTES_PER_GB) @@ -448,8 +440,7 @@ def _worker_tag(worker: dict, role_prefix: str) -> str: ep = worker.get(Fields.EP.value, 1) dpa = worker.get(Fields.DP_ATTN.value, False) tag = ( - f"{role_prefix}{worker[Fields.NUM_WORKER.value]}" - f"x{worker[Fields.TP.value]}" + f"{role_prefix}{worker[Fields.NUM_WORKER.value]}x{worker[Fields.TP.value]}" ) if ep != 1: tag += f"ep{ep}" @@ -477,16 +468,17 @@ def component_metadata(benchmark: dict, config: dict) -> dict: def chunk_multinode_agentic_concurrencies(conc_values: list[int]) -> list[list[int]]: """Bound sequential agentic profiles sharing one server allocation.""" size = MAX_MULTINODE_AGENTIC_CONCURRENCIES_PER_ALLOCATION - return [conc_values[index:index + size] for index in range(0, len(conc_values), size)] + return [ + conc_values[index : index + size] for index in range(0, len(conc_values), size) + ] def _freeze_matrix_value(value): """Convert nested matrix values into hashable equivalents.""" if isinstance(value, dict): - return tuple(sorted( - (key, _freeze_matrix_value(item)) - for key, item in value.items() - )) + return tuple( + sorted((key, _freeze_matrix_value(item)) for key, item in value.items()) + ) if isinstance(value, list): return tuple(_freeze_matrix_value(item) for item in value) return value @@ -510,14 +502,18 @@ def _multinode_parallelism_key(entry: dict) -> tuple: Fields.EVAL_ALL_CONCS.value, Fields.EXP_NAME.value, } - return tuple(sorted( - (key, _freeze_matrix_value(value)) - for key, value in entry.items() - if key not in ignored_fields - )) + return tuple( + sorted( + (key, _freeze_matrix_value(value)) + for key, value in entry.items() + if key not in ignored_fields + ) + ) -def mark_eval_entries(matrix_values: list[dict], include_agentic: bool = False) -> list[dict]: +def mark_eval_entries( + matrix_values: list[dict], include_agentic: bool = False +) -> list[dict]: """Eval selection policy: - Single-node: only consider 8k1k (isl=8192, osl=1024). For each unique (model, runner, framework, precision, isl, osl, spec-decoding, dp-attn): @@ -554,7 +550,10 @@ def _eligible_eval_concs(entry): for i, entry in enumerate(matrix_values): if Fields.TP.value not in entry: continue - if entry.get(Fields.ISL.value) != target_isl or entry.get(Fields.OSL.value) != target_osl: + if ( + entry.get(Fields.ISL.value) != target_isl + or entry.get(Fields.OSL.value) != target_osl + ): continue if not _eligible_eval_concs(entry): continue @@ -586,7 +585,10 @@ def _eligible_eval_concs(entry): continue if Fields.PREFILL.value not in entry: continue - if entry.get(Fields.ISL.value) != target_isl or entry.get(Fields.OSL.value) != target_osl: + if ( + entry.get(Fields.ISL.value) != target_isl + or entry.get(Fields.OSL.value) != target_osl + ): continue eval_concs = _eligible_eval_concs(entry) if not eval_concs: @@ -606,13 +608,15 @@ def _eligible_eval_concs(entry): # The selected eval subset uses exactly one conc per group. ag_mn_groups = defaultdict(list) for i, entry in enumerate(matrix_values): - if entry.get(Fields.SCENARIO_TYPE.value) != 'agentic-coding': + if entry.get(Fields.SCENARIO_TYPE.value) != "agentic-coding": continue if Fields.PREFILL.value in entry: eval_concs = _eligible_eval_concs(entry) if not eval_concs: continue - ag_mn_groups[_multinode_parallelism_key(entry)].append((i, eval_concs[-1])) + ag_mn_groups[_multinode_parallelism_key(entry)].append( + (i, eval_concs[-1]) + ) continue conc = entry[Fields.CONC.value] conc_val = max(conc) if isinstance(conc, list) else conc @@ -659,7 +663,7 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: target_isl, target_osl = seq_len_stoi["8k1k"] for entry in matrix_values: - if entry.get(Fields.SCENARIO_TYPE.value) == 'agentic-coding': + if entry.get(Fields.SCENARIO_TYPE.value) == "agentic-coding": if Fields.PREFILL.value not in entry: entry[Fields.RUN_EVAL.value] = True expanded_entries.append(entry) @@ -700,9 +704,9 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: parallelism_key = _multinode_parallelism_key(entry) if parallelism_key in multinode_indices: existing = expanded_entries[multinode_indices[parallelism_key]] - existing[Fields.CONC.value] = sorted(set( - existing[Fields.CONC.value] + conc_values - )) + existing[Fields.CONC.value] = sorted( + set(existing[Fields.CONC.value] + conc_values) + ) continue batched_entry = { @@ -748,7 +752,8 @@ def generate_full_sweep(args, all_config_data, runner_data): if invalid_runners: raise ValueError( f"Invalid runner type(s): {invalid_runners}. " - f"Valid runner types are: {', '.join(sorted(valid_runner_types))}") + f"Valid runner types are: {', '.join(sorted(valid_runner_types))}" + ) matrix_values = [] @@ -757,7 +762,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.seq_lens: seq_lens_filter = {seq_len_stoi[sl] for sl in args.seq_lens} - # Iterate through all configurations and apply filters as specified (this is just "selecting" + # Iterate through all configurations and apply filters as specified (this is just "selecting" # configs from all of the master configs subject to some pattern matching) for key, val in all_config_data.items(): # Filter by model prefix if specified @@ -783,8 +788,14 @@ def generate_full_sweep(args, all_config_data, runner_data): disagg = val.get(Fields.DISAGG.value, False) scenarios = val[Fields.SCENARIOS.value] - scenario_filter = set(args.scenario_type) if getattr(args, 'scenario_type', None) else None - seq_len_configs = scenarios.get(Fields.FIXED_SEQ_LEN.value, []) if (scenario_filter is None or 'fixed-seq-len' in scenario_filter) else [] + scenario_filter = ( + set(args.scenario_type) if getattr(args, "scenario_type", None) else None + ) + seq_len_configs = ( + scenarios.get(Fields.FIXED_SEQ_LEN.value, []) + if (scenario_filter is None or "fixed-seq-len" in scenario_filter) + else [] + ) image = val[Fields.IMAGE.value] model = val[Fields.MODEL.value] precision = val[Fields.PRECISION.value] @@ -797,7 +808,8 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.runner_node_filter: runner_nodes = runner_nodes_for_label(runner, runner_data) runner_nodes_to_use = [ - node for node in runner_nodes if args.runner_node_filter in node] + node for node in runner_nodes if args.runner_node_filter in node + ] if not runner_nodes_to_use: # No matching nodes for this config's runner type, skip this config continue @@ -867,7 +879,9 @@ def generate_full_sweep(args, all_config_data, runner_data): conc_values = filtered_conc seq_len_str = seq_len_to_str(isl, osl) - runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] + runners_for_entry = ( + runner_nodes_to_use if runner_nodes_to_use else [runner] + ) for runner_value in runners_for_entry: entry = { @@ -930,8 +944,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.min_conc <= 0: continue conc_values = [ - conc for conc in conc_values - if conc >= args.min_conc + conc for conc in conc_values if conc >= args.min_conc ] if not conc_values: continue @@ -940,13 +953,10 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.max_conc <= 0: continue filtered_conc = [ - conc for conc in conc_values - if conc <= args.max_conc + conc for conc in conc_values if conc <= args.max_conc ] conc_values = ( - filtered_conc - if filtered_conc - else [args.max_conc] + filtered_conc if filtered_conc else [args.max_conc] ) else: conc_start = bmk[Fields.CONC_START.value] @@ -981,7 +991,9 @@ def generate_full_sweep(args, all_config_data, runner_data): conc = conc_end seq_len_str = seq_len_to_str(isl, osl) - runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] + runners_for_entry = ( + runner_nodes_to_use if runner_nodes_to_use else [runner] + ) for conc in conc_values: for runner_value in runners_for_entry: @@ -1018,7 +1030,11 @@ def generate_full_sweep(args, all_config_data, runner_data): matrix_values.append(entry) # ---- Agentic-coding scenarios ---- - agentic_configs = scenarios.get(Fields.AGENTIC_CODING.value, []) if (scenario_filter is None or 'agentic-coding' in scenario_filter) else [] + agentic_configs = ( + scenarios.get(Fields.AGENTIC_CODING.value, []) + if (scenario_filter is None or "agentic-coding" in scenario_filter) + else [] + ) if is_multinode and not args.multi_node: continue if not is_multinode and not args.single_node: @@ -1045,7 +1061,8 @@ def generate_full_sweep(args, all_config_data, runner_data): kv_offloading = bmk[Fields.KV_OFFLOADING.value] kv_offload_backend = bmk.get(Fields.KV_OFFLOAD_BACKEND.value) total_cpu_dram_gb = agentic_dram_offload_gb( - agentic_config, bmk, runner, runner_data) + agentic_config, bmk, runner, runner_data + ) # Get concurrency values conc_list = bmk.get(Fields.CONC_LIST.value) @@ -1072,7 +1089,9 @@ def generate_full_sweep(args, all_config_data, runner_data): if not conc_values: continue - runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] + runners_for_entry = ( + runner_nodes_to_use if runner_nodes_to_use else [runner] + ) if is_multinode: # Preserve historical exp-names for the default (no offload) @@ -1083,7 +1102,9 @@ def generate_full_sweep(args, all_config_data, runner_data): else "" ) for runner_value in runners_for_entry: - for conc_batch in chunk_multinode_agentic_concurrencies(conc_values): + for conc_batch in chunk_multinode_agentic_concurrencies( + conc_values + ): entry = { Fields.IMAGE.value: image, Fields.MODEL.value: model, @@ -1099,13 +1120,19 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb, Fields.DURATION.value: duration, Fields.EXP_NAME.value: multinode_agentic_exp_name( - model_code, prefill, decode, conc_batch, offload_suffix + model_code, + prefill, + decode, + conc_batch, + offload_suffix, ), Fields.DISAGG.value: disagg, Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend + entry[Fields.KV_OFFLOAD_BACKEND.value] = ( + kv_offload_backend + ) entry.update(component_metadata(bmk, val)) add_multinode_node_count( entry, @@ -1129,7 +1156,9 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.DCP_SIZE.value: dcp_size, Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, + Fields.DP_ATTN.value: dp_attn + if dp_attn is not None + else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.CONC.value: conc, Fields.KV_OFFLOADING.value: kv_offloading, @@ -1138,12 +1167,18 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.EXP_NAME.value: ( f"{model_code}_tp{tp}_conc{conc}_" f"{agentic_kv_offload_suffix(kv_offloading, kv_offload_backend)}" - + (f"_spec-{spec_decoding}" if spec_decoding != "none" else "") + + ( + f"_spec-{spec_decoding}" + if spec_decoding != "none" + else "" + ) ), Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend + entry[Fields.KV_OFFLOAD_BACKEND.value] = ( + kv_offload_backend + ) entry.update(component_metadata(bmk, val)) validate_agentic_matrix_entry(entry) matrix_values.append(entry) @@ -1151,7 +1186,9 @@ def generate_full_sweep(args, all_config_data, runner_data): return matrix_values -def _runner_values_for_filter(runner: str, runner_data: dict, runner_node_filter: str | None) -> list[str]: +def _runner_values_for_filter( + runner: str, runner_data: dict, runner_node_filter: str | None +) -> list[str]: if not runner_node_filter: return [runner] @@ -1191,18 +1228,25 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): framework = val[Fields.FRAMEWORK.value] runner = val[Fields.RUNNER.value] runners_for_entry = _runner_values_for_filter( - runner, runner_data, getattr(args, 'runner_node_filter', None)) + runner, runner_data, getattr(args, "runner_node_filter", None) + ) if not runners_for_entry: continue disagg = val.get(Fields.DISAGG.value, False) # Build seq-len filter if --seq-lens was provided seq_lens_filter = None - if getattr(args, 'seq_lens', None): + if getattr(args, "seq_lens", None): seq_lens_filter = {seq_len_stoi[s] for s in args.seq_lens} - scenario_filter = set(args.scenario_type) if getattr(args, 'scenario_type', None) else None - fixed_configs = val[Fields.SCENARIOS.value].get(Fields.FIXED_SEQ_LEN.value, []) if (scenario_filter is None or 'fixed-seq-len' in scenario_filter) else [] + scenario_filter = ( + set(args.scenario_type) if getattr(args, "scenario_type", None) else None + ) + fixed_configs = ( + val[Fields.SCENARIOS.value].get(Fields.FIXED_SEQ_LEN.value, []) + if (scenario_filter is None or "fixed-seq-len" in scenario_filter) + else [] + ) for seq_len_config in fixed_configs: isl = seq_len_config[Fields.ISL.value] osl = seq_len_config[Fields.OSL.value] @@ -1235,7 +1279,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): conc = conc_end # Apply --conc filter if provided (only for test-config) - if getattr(args, 'conc', None): + if getattr(args, "conc", None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: # No intersection with requested conc values; skip @@ -1266,7 +1310,9 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): runner_data, bmk.get(Fields.NUM_NODES.value), ) - matrix_values.append(validate_matrix_entry(entry, is_multinode=True)) + matrix_values.append( + validate_matrix_entry(entry, is_multinode=True) + ) else: # Single-node config tp = bmk[Fields.TP.value] @@ -1294,7 +1340,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): conc = conc_end # Apply --conc filter if provided (only for test-config) - if getattr(args, 'conc', None): + if getattr(args, "conc", None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: # No intersection with requested conc values; skip @@ -1318,17 +1364,25 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.CONC.value: conc, Fields.MAX_MODEL_LEN.value: isl + osl + 256, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, + Fields.DP_ATTN.value: dp_attn + if dp_attn is not None + else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.EXP_NAME.value: f"{model_code}_{seq_len_str}", Fields.DISAGG.value: disagg, Fields.RUN_EVAL.value: False, } entry.update(component_metadata(bmk, val)) - matrix_values.append(validate_matrix_entry(entry, is_multinode=False)) + matrix_values.append( + validate_matrix_entry(entry, is_multinode=False) + ) # ---- Agentic-coding scenarios ---- - agentic_configs = val[Fields.SCENARIOS.value].get(Fields.AGENTIC_CODING.value, []) if (scenario_filter is None or 'agentic-coding' in scenario_filter) else [] + agentic_configs = ( + val[Fields.SCENARIOS.value].get(Fields.AGENTIC_CODING.value, []) + if (scenario_filter is None or "agentic-coding" in scenario_filter) + else [] + ) for agentic_config in agentic_configs: duration = DEFAULT_AGENTIC_DURATION_SECONDS bmk_space = agentic_config[Fields.SEARCH_SPACE.value] @@ -1350,7 +1404,8 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): kv_offloading = bmk[Fields.KV_OFFLOADING.value] kv_offload_backend = bmk.get(Fields.KV_OFFLOAD_BACKEND.value) total_cpu_dram_gb = agentic_dram_offload_gb( - agentic_config, bmk, runner, runner_data) + agentic_config, bmk, runner, runner_data + ) conc_list = bmk.get(Fields.CONC_LIST.value) if conc_list: @@ -1368,7 +1423,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): if conc > conc_end: conc = conc_end - if getattr(args, 'conc', None): + if getattr(args, "conc", None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: continue @@ -1382,7 +1437,9 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): else "" ) for runner_value in runners_for_entry: - for conc_batch in chunk_multinode_agentic_concurrencies(conc_values): + for conc_batch in chunk_multinode_agentic_concurrencies( + conc_values + ): entry = { Fields.IMAGE.value: image, Fields.MODEL.value: model, @@ -1398,13 +1455,19 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb, Fields.DURATION.value: duration, Fields.EXP_NAME.value: multinode_agentic_exp_name( - model_code, prefill, decode, conc_batch, offload_suffix + model_code, + prefill, + decode, + conc_batch, + offload_suffix, ), Fields.DISAGG.value: disagg, Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend + entry[Fields.KV_OFFLOAD_BACKEND.value] = ( + kv_offload_backend + ) entry.update(component_metadata(bmk, val)) add_multinode_node_count( entry, @@ -1427,7 +1490,9 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.DCP_SIZE.value: dcp_size, Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, + Fields.DP_ATTN.value: dp_attn + if dp_attn is not None + else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.CONC.value: conc, Fields.KV_OFFLOADING.value: kv_offloading, @@ -1436,12 +1501,18 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.EXP_NAME.value: ( f"{model_code}_tp{tp}_conc{conc}_" f"{agentic_kv_offload_suffix(kv_offloading, kv_offload_backend)}" - + (f"_spec-{spec_decoding}" if spec_decoding != "none" else "") + + ( + f"_spec-{spec_decoding}" + if spec_decoding != "none" + else "" + ) ), Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend + entry[Fields.KV_OFFLOAD_BACKEND.value] = ( + kv_offload_backend + ) entry.update(component_metadata(bmk, val)) matrix_values.append(validate_agentic_matrix_entry(entry)) @@ -1460,7 +1531,7 @@ def expand_config_keys(config_keys, available_keys): available = list(available_keys) seen = {} # use dict to preserve insertion order for key in config_keys: - if '*' in key or '?' in key: + if "*" in key or "?" in key: matches = fnmatch.filter(available, key) if not matches: raise ValueError( @@ -1479,9 +1550,32 @@ def expand_config_keys(config_keys, available_keys): return list(seen) +def filter_exp_names(entries: list[dict], exp_names: list[str]) -> list[dict]: + """Select exact generated experiment identities and reject ambiguity.""" + requested = set(exp_names) + if len(requested) != len(exp_names): + raise ValueError("--exp-names contains duplicate values") + + matches: dict[str, int] = {name: 0 for name in exp_names} + for entry in entries: + exp_name = entry.get(Fields.EXP_NAME.value) + if exp_name in matches: + matches[exp_name] += 1 + + missing = sorted(name for name, count in matches.items() if count == 0) + ambiguous = sorted(name for name, count in matches.items() if count > 1) + if missing: + raise ValueError("Experiment name(s) not found: " + ", ".join(missing)) + if ambiguous: + raise ValueError( + "Experiment name(s) matched multiple rows: " + ", ".join(ambiguous) + ) + return [entry for entry in entries if entry.get(Fields.EXP_NAME.value) in requested] + + def apply_node_type_defaults(args): """Default both single_node and multi_node to True when neither is specified.""" - if hasattr(args, 'single_node') and hasattr(args, 'multi_node'): + if hasattr(args, "single_node") and hasattr(args, "multi_node"): if not args.single_node and not args.multi_node: args.single_node = True args.multi_node = True @@ -1492,191 +1586,194 @@ def main(): # Create parent parser with common arguments parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - '--config-files', - nargs='+', + "--config-files", + nargs="+", required=True, - help='One or more configuration files (YAML format)' + help="One or more configuration files (YAML format)", ) parent_parser.add_argument( - '--runner-config', - default='configs/runners.yaml', - help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' + "--runner-config", + default="configs/runners.yaml", + help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", ) eval_group = parent_parser.add_mutually_exclusive_group() eval_group.add_argument( - '--no-evals', - action='store_true', - help='When specified, skip evals (throughput benchmarks only).' + "--no-evals", + action="store_true", + help="When specified, skip evals (throughput benchmarks only).", ) eval_group.add_argument( - '--evals-only', - action='store_true', - help='When specified, run ONLY the eval subset (excludes non-eval configs).' + "--evals-only", + action="store_true", + help="When specified, run ONLY the eval subset (excludes non-eval configs).", ) parent_parser.add_argument( - '--all-evals', - action='store_true', + "--all-evals", + action="store_true", help=( - 'Expand eval selection to every generated fixed-sequence config. ' - 'Can be combined with --evals-only; used alone, it also emits eval-only jobs.' - ) + "Expand eval selection to every generated fixed-sequence config. " + "Can be combined with --evals-only; used alone, it also emits eval-only jobs." + ), ) parent_parser.add_argument( - '--trim-conc', - action='store_true', + "--trim-conc", + action="store_true", help=( - 'Trim each generated deployment shape to its minimum concurrency ' - 'after applying eval selection.' - ) + "Trim each generated deployment shape to its minimum concurrency " + "after applying eval selection." + ), ) parent_parser.add_argument( - '--runner-node-filter', + "--runner-node-filter", required=False, - help='Filter runner nodes by substring match (e.g., "amd" to only include nodes containing that string). Expands each config to individual matching nodes.' + help='Filter runner nodes by substring match (e.g., "amd" to only include nodes containing that string). Expands each config to individual matching nodes.', ) parent_parser.add_argument( - '--scenario-type', - nargs='+', - choices=['fixed-seq-len', 'agentic-coding'], + "--scenario-type", + nargs="+", + choices=["fixed-seq-len", "agentic-coding"], required=False, - help='Scenario type(s) to include. If not specified, all scenario types are generated.' + help="Scenario type(s) to include. If not specified, all scenario types are generated.", ) # Create main parser parser = argparse.ArgumentParser( - description='Generate benchmark configurations from YAML config files' + description="Generate benchmark configurations from YAML config files" ) # Create subparsers for subcommands subparsers = parser.add_subparsers( - dest='command', - required=True, - help='Available commands' + dest="command", required=True, help="Available commands" ) # Subcommand: full-sweep full_sweep_parser = subparsers.add_parser( - 'full-sweep', + "full-sweep", parents=[parent_parser], add_help=False, - help='Generate full sweep configurations with optional filtering by model, precision, framework, runner type, and sequence lengths' + help="Generate full sweep configurations with optional filtering by model, precision, framework, runner type, and sequence lengths", ) full_sweep_parser.add_argument( - '--model-prefix', - nargs='+', + "--model-prefix", + nargs="+", required=False, - help='Model prefix(es) to filter configurations (optional, can specify multiple)' + help="Model prefix(es) to filter configurations (optional, can specify multiple)", ) full_sweep_parser.add_argument( - '--precision', - nargs='+', + "--precision", + nargs="+", required=False, - help='Precision(s) to filter by (e.g., fp4, fp8) (optional, can specify multiple)' + help="Precision(s) to filter by (e.g., fp4, fp8) (optional, can specify multiple)", ) full_sweep_parser.add_argument( - '--framework', - nargs='+', + "--framework", + nargs="+", required=False, - help='Framework(s) to filter by (e.g., vllm, trt, sglang) (optional, can specify multiple)' + help="Framework(s) to filter by (e.g., vllm, trt, sglang) (optional, can specify multiple)", ) full_sweep_parser.add_argument( - '--runner-type', - nargs='+', + "--runner-type", + nargs="+", required=False, - help='Runner type(s) to filter by (e.g., h200, h100) (optional, can specify multiple)' + help="Runner type(s) to filter by (e.g., h200, h100) (optional, can specify multiple)", ) full_sweep_parser.add_argument( - '--seq-lens', - nargs='+', + "--seq-lens", + nargs="+", choices=list(seq_len_stoi.keys()), required=False, - help=f"Sequence length configurations to include: {', '.join(seq_len_stoi.keys())}. If not specified, all sequence lengths are included." + help=f"Sequence length configurations to include: {', '.join(seq_len_stoi.keys())}. If not specified, all sequence lengths are included.", ) full_sweep_parser.add_argument( - '--step-size', + "--step-size", type=int, default=2, - help='Step size for concurrency values (default: 2)' + help="Step size for concurrency values (default: 2)", ) full_sweep_parser.add_argument( - '--min-conc', + "--min-conc", type=int, required=False, - help='Minimum concurrency value to include (filters out lower concurrency values)' + help="Minimum concurrency value to include (filters out lower concurrency values)", ) full_sweep_parser.add_argument( - '--max-conc', + "--max-conc", type=int, required=False, - help='Maximum concurrency value to include (filters out higher concurrency values)' + help="Maximum concurrency value to include (filters out higher concurrency values)", ) full_sweep_parser.add_argument( - '--max-tp', + "--max-tp", type=int, required=False, - help='Maximum tensor parallelism value to include (single-node only)' + help="Maximum tensor parallelism value to include (single-node only)", ) full_sweep_parser.add_argument( - '--max-ep', + "--max-ep", type=int, required=False, - help='Maximum expert parallelism value to include (single-node only)' + help="Maximum expert parallelism value to include (single-node only)", ) full_sweep_parser.add_argument( - '--single-node', - action='store_true', - help='Only generate single-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.' + "--single-node", + action="store_true", + help="Only generate single-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.", ) full_sweep_parser.add_argument( - '--multi-node', - action='store_true', - help='Only generate multi-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.' + "--multi-node", + action="store_true", + help="Only generate multi-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.", ) full_sweep_parser.add_argument( - '-h', '--help', - action='help', - help='Show this help message and exit' + "-h", "--help", action="help", help="Show this help message and exit" ) # Subcommand: test-config test_config_keys_parser = subparsers.add_parser( - 'test-config', + "test-config", parents=[parent_parser], add_help=False, - help='Generate full sweep for specific config keys. Validates that all specified keys exist before generating.' + help="Generate full sweep for specific config keys. Validates that all specified keys exist before generating.", ) test_config_keys_parser.add_argument( - '--config-keys', - nargs='+', + "--config-keys", + nargs="+", required=True, - help='One or more config keys to generate sweep for (e.g., dsr1-fp4-b200-sglang dsr1-fp8-h200-trt)' + help="One or more config keys to generate sweep for (e.g., dsr1-fp4-b200-sglang dsr1-fp8-h200-trt)", ) test_config_keys_parser.add_argument( - '--conc', - nargs='+', + "--conc", + nargs="+", type=int, required=False, - help='Only include these concurrency values. Values must exist in the config conc-range/list.' + help="Only include these concurrency values. Values must exist in the config conc-range/list.", ) test_config_keys_parser.add_argument( - '--seq-lens', - nargs='+', + "--exp-names", + nargs="+", + required=False, + help=( + "Only include exact generated experiment names. Each name must " + "match exactly one row after config and concurrency filtering." + ), + ) + test_config_keys_parser.add_argument( + "--seq-lens", + nargs="+", choices=list(seq_len_stoi.keys()), required=False, - help='Only include these sequence length configurations (e.g., 1k1k 8k1k)' + help="Only include these sequence length configurations (e.g., 1k1k 8k1k)", ) test_config_keys_parser.add_argument( - '-h', '--help', - action='help', - help='Show this help message and exit' + "-h", "--help", action="help", help="Show this help message and exit" ) args = parser.parse_args() apply_node_type_defaults(args) - if args.command == 'full-sweep' and args.step_size <= 1: + if args.command == "full-sweep" and args.step_size <= 1: parser.error("--step-size must be greater than 1") if ( - args.command == 'full-sweep' + args.command == "full-sweep" and args.min_conc is not None and args.max_conc is not None and args.min_conc > args.max_conc @@ -1690,17 +1787,24 @@ def main(): runner_data = load_runner_file(args.runner_config) # Route to appropriate function based on subcommand - if args.command == 'full-sweep': + if args.command == "full-sweep": matrix_values = generate_full_sweep(args, all_config_data, runner_data) - elif args.command == 'test-config': + elif args.command == "test-config": matrix_values = generate_test_config_sweep(args, all_config_data, runner_data) else: parser.error(f"Unknown command: {args.command}") - + + if args.command == "test-config" and args.exp_names: + try: + matrix_values = filter_exp_names(matrix_values, args.exp_names) + except ValueError as error: + parser.error(str(error)) # Apply the existing eval policy first, then expand it when requested. if not args.no_evals: - matrix_values = mark_eval_entries(matrix_values, include_agentic=args.evals_only or args.all_evals) + matrix_values = mark_eval_entries( + matrix_values, include_agentic=args.evals_only or args.all_evals + ) if args.all_evals: matrix_values = mark_all_eval_entries(matrix_values) @@ -1708,7 +1812,9 @@ def main(): matrix_values = trim_conc(matrix_values) if args.evals_only or args.all_evals: - matrix_values = [e for e in matrix_values if e.get(Fields.RUN_EVAL.value, False)] + matrix_values = [ + e for e in matrix_values if e.get(Fields.RUN_EVAL.value, False) + ] for entry in matrix_values: entry[Fields.EVAL_ONLY.value] = True diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index 09dc638fe6..6c700a58d2 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -1,4 +1,5 @@ """Comprehensive tests for generate_sweep_configs.py""" + import argparse import copy import hashlib @@ -11,6 +12,7 @@ add_multinode_node_count, apply_node_type_defaults, expand_config_keys, + filter_exp_names, generate_full_sweep, generate_test_config_sweep, mark_all_eval_entries, @@ -101,9 +103,10 @@ def test_multinode_node_count_uses_role_gpu_footprints(sample_runner_config): prefill = {"num-worker": 3, "tp": 2, "pp": 1, "pcp-size": 1} decode = {"num-worker": 2, "tp": 8, "pp": 1, "pcp-size": 1} - assert multinode_node_count( - prefill, decode, "cluster:b300-nv", sample_runner_config - ) == 3 + assert ( + multinode_node_count(prefill, decode, "cluster:b300-nv", sample_runner_config) + == 3 + ) def test_multinode_node_count_honors_explicit_role_node_settings(): @@ -127,9 +130,7 @@ def test_multinode_node_count_resolves_heterogeneous_worker_hardware( prefill = {"hardware": "gb200", "num-worker": 5, "tp": 4} decode = {"hardware": "h100", "num-worker": 1, "tp": 8} - assert multinode_node_count( - prefill, decode, "gb200", sample_runner_config - ) == 6 + assert multinode_node_count(prefill, decode, "gb200", sample_runner_config) == 6 def test_multinode_node_count_prefers_checked_in_recipe_resources( @@ -147,9 +148,10 @@ def test_multinode_node_count_prefers_checked_in_recipe_resources( } decode = {"num-worker": 1, "tp": 1} - assert multinode_node_count( - prefill, decode, "cluster:gb200-nv", sample_runner_config - ) == 7 + assert ( + multinode_node_count(prefill, decode, "cluster:gb200-nv", sample_runner_config) + == 7 + ) def test_multinode_node_count_resolves_repo_relative_recipe_path( @@ -168,15 +170,17 @@ def test_multinode_node_count_resolves_repo_relative_recipe_path( } decode = {"num-worker": 0, "tp": 8} - assert multinode_node_count( - prefill, decode, "cluster:gb300-nv", sample_runner_config - ) == 2 + assert ( + multinode_node_count(prefill, decode, "cluster:gb300-nv", sample_runner_config) + == 2 + ) # ============================================================================= # Test Fixtures # ============================================================================= + @pytest.fixture def sample_single_node_config(): """Single node config based on dsr1-fp8-mi300x-sglang.""" @@ -191,23 +195,18 @@ def sample_single_node_config(): "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, - "search-space": [ - {"tp": 8, "conc-start": 4, "conc-end": 64} - ] + "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 64}], }, { "isl": 8192, "osl": 1024, - "search-space": [ - {"tp": 8, "conc-start": 4, "conc-end": 64} - ] - } + "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 64}], + }, ] - } + }, } } @@ -228,7 +227,6 @@ def sample_multinode_config(): "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, @@ -258,10 +256,10 @@ def sample_multinode_config(): ], }, } - ] + ], } ] - } + }, } } @@ -280,12 +278,27 @@ def sample_runner_config(): "gb200": ["gb200-nv_0"], }, "hardware": { - "cluster:h100-dgxc": {"available-cpu-dram-mib": 2063837, "gpus-per-node": 8}, - "cluster:h200-dgxc": {"available-cpu-dram-mib": 1471356, "gpus-per-node": 8}, - "cluster:b200-nscale": {"available-cpu-dram-mib": 3774874, "gpus-per-node": 8}, + "cluster:h100-dgxc": { + "available-cpu-dram-mib": 2063837, + "gpus-per-node": 8, + }, + "cluster:h200-dgxc": { + "available-cpu-dram-mib": 1471356, + "gpus-per-node": 8, + }, + "cluster:b200-nscale": { + "available-cpu-dram-mib": 3774874, + "gpus-per-node": 8, + }, "cluster:b300-nv": {"available-cpu-dram-mib": 2964436, "gpus-per-node": 8}, - "cluster:mi300x-amds": {"available-cpu-dram-mib": 2321924, "gpus-per-node": 8}, - "cluster:mi355x-amds": {"available-cpu-dram-mib": 3095781, "gpus-per-node": 8}, + "cluster:mi300x-amds": { + "available-cpu-dram-mib": 2321924, + "gpus-per-node": 8, + }, + "cluster:mi355x-amds": { + "available-cpu-dram-mib": 3095781, + "gpus-per-node": 8, + }, "cluster:gb200-nv": {"available-cpu-dram-mib": 860160, "gpus-per-node": 4}, }, } @@ -335,6 +348,7 @@ def full_sweep_args_multi_node(): # Test seq_len mappings # ============================================================================= + class TestSeqLenMappings: """Tests for sequence length string mappings.""" @@ -367,6 +381,7 @@ def test_unknown_sequence_lengths(self): # Test mark_eval_entries # ============================================================================= + class TestMarkEvalEntries: """Tests for eval matrix selection policy.""" @@ -374,13 +389,21 @@ def test_marks_agentic_entry_for_gsm8k(self): matrix_values = [ { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "vllm", - "precision": "fp4", "tp": 8, "conc": 32, + "model": "m", + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": 32, }, { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "vllm", - "precision": "fp4", "tp": 8, "conc": 64, + "model": "m", + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": 64, }, ] @@ -402,8 +425,12 @@ def test_marks_multinode_agentic_entry_at_highest_eligible_conc(self): """ common = { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "sglang-disagg", - "precision": "fp4", "spec-decoding": "none", "disagg": True, + "model": "m", + "runner": "b300", + "framework": "sglang-disagg", + "precision": "fp4", + "spec-decoding": "none", + "disagg": True, "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, } @@ -425,8 +452,12 @@ def test_multinode_agentic_groups_are_independent_per_topology(self): prefill EP/DP) must each get their own eval row.""" base = { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "sglang-disagg", - "precision": "fp4", "spec-decoding": "none", "disagg": True, + "model": "m", + "runner": "b300", + "framework": "sglang-disagg", + "precision": "fp4", + "spec-decoding": "none", + "disagg": True, } topology_a = { "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, @@ -454,13 +485,21 @@ def test_default_mode_does_not_mark_agentic(self): matrix_values = [ { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "vllm", - "precision": "fp4", "tp": 8, "conc": 32, + "model": "m", + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": 32, }, { "scenario-type": "agentic-coding", - "model": "m", "runner": "b300", "framework": "vllm", - "precision": "fp4", "tp": 8, "conc": 64, + "model": "m", + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": 64, }, ] @@ -621,6 +660,7 @@ def test_multi_node_marks_each_parallelism_at_highest_eligible_conc(self): def test_multi_node_worker_counts_define_parallelism(self): """Prefill and decode worker counts should each define a distinct eval target.""" + def entry(prefill_workers, decode_workers, conc): return { "model": "deepseek-ai/DeepSeek-R1-0528", @@ -645,11 +685,13 @@ def entry(prefill_workers, decode_workers, conc): "conc": [16, conc], } - result = mark_eval_entries([ - entry(prefill_workers=1, decode_workers=1, conc=32), - entry(prefill_workers=2, decode_workers=1, conc=64), - entry(prefill_workers=1, decode_workers=2, conc=128), - ]) + result = mark_eval_entries( + [ + entry(prefill_workers=1, decode_workers=1, conc=32), + entry(prefill_workers=2, decode_workers=1, conc=64), + entry(prefill_workers=1, decode_workers=2, conc=128), + ] + ) assert [(e["run-eval"], e["eval-conc"]) for e in result] == [ (True, 32), @@ -698,50 +740,111 @@ def test_multi_node_split_parallelism_uses_only_highest_concurrency_entry(self): def test_marks_highest_and_median_conc(self): """Should mark highest and median concurrency for 8k1k entries.""" entries = [ - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 32, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 128, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 512, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "tp": 2, + "conc": 32, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + }, + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "tp": 2, + "conc": 128, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + }, + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "tp": 2, + "conc": 512, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + }, ] result = mark_eval_entries(entries) # conc values: [32, 128, 512]. median=128 (index 1), highest=512 - assert result[0]['run-eval'] is False # conc=32 - assert result[1]['run-eval'] is True # conc=128 (median) - assert result[2]['run-eval'] is True # conc=512 (highest) + assert result[0]["run-eval"] is False # conc=32 + assert result[1]["run-eval"] is True # conc=128 (median) + assert result[2]["run-eval"] is True # conc=512 (highest) def test_non_8k1k_never_marked(self): """Entries with non-8k1k seq lengths should never be eval-marked.""" entries = [ - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 512, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 1024, + "osl": 1024, + "tp": 2, + "conc": 512, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + }, ] result = mark_eval_entries(entries) - assert result[0]['run-eval'] is False + assert result[0]["run-eval"] is False def test_never_marks_all_entries(self): """mark_eval_entries should never mark every single-node entry, ensuring the e2e splitting logic can distinguish default from evals-only.""" entries = [ - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': c, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False} + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "tp": 2, + "conc": c, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + } for c in [32, 64, 128, 256, 512] ] + [ # Non-8k1k entry that should never be marked - {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 64, - 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, + { + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 1024, + "osl": 1024, + "tp": 2, + "conc": 64, + "spec-decoding": False, + "dp-attn": False, + "run-eval": False, + }, ] result = mark_eval_entries(entries) - non_prefill = [x for x in result if 'prefill' not in x] - assert not all(x['run-eval'] for x in non_prefill), \ + non_prefill = [x for x in result if "prefill" not in x] + assert not all(x["run-eval"] for x in non_prefill), ( "mark_eval_entries must not mark all entries — would break e2e splitting" + ) class TestMarkAllEvalEntries: @@ -750,150 +853,202 @@ class TestMarkAllEvalEntries: def test_marks_only_8k1k_entries_and_passes_other_seq_lens_through(self): entries = [ { # 1k1k is not eligible for evals -> left unmarked - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 1, - 'spec-decoding': 'none', 'dp-attn': False, 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 1024, + "osl": 1024, + "tp": 2, + "conc": 1, + "spec-decoding": "none", + "dp-attn": False, + "run-eval": False, }, { # 8k1k is eligible -> marked for eval - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 8, - 'spec-decoding': 'none', 'dp-attn': False, 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "tp": 2, + "conc": 8, + "spec-decoding": "none", + "dp-attn": False, + "run-eval": False, }, ] result = mark_all_eval_entries(entries) - by_isl = {entry['isl']: entry for entry in result} - assert by_isl[1024]['run-eval'] is False - assert by_isl[8192]['run-eval'] is True + by_isl = {entry["isl"]: entry for entry in result} + assert by_isl[1024]["run-eval"] is False + assert by_isl[8192]["run-eval"] is True def test_batches_every_multinode_concurrency_per_engine_topology(self): entries = [ { - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [1, 4, 8, 16], - 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [1, 4, 8, 16], + "run-eval": False, }, { - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': True}, - 'decode': {'dp-attn': False}, - 'conc': [32], - 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": True}, + "decode": {"dp-attn": False}, + "conc": [32], + "run-eval": False, }, ] result = mark_all_eval_entries(entries) assert len(result) == 2 - assert all(entry['run-eval'] for entry in result) - assert [entry['conc'] for entry in result] == [ - [1, 4, 8, 16], [32], + assert all(entry["run-eval"] for entry in result) + assert [entry["conc"] for entry in result] == [ + [1, 4, 8, 16], + [32], ] - assert all(entry['eval-all-concs'] is True for entry in result) - assert all('eval-conc' not in entry for entry in result) + assert all(entry["eval-all-concs"] is True for entry in result) + assert all("eval-conc" not in entry for entry in result) def test_default_eval_selection_does_not_collapse_all_evals_expansion(self): entries = [ { - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [1, 4, 8, 16, 32], - 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [1, 4, 8, 16, 32], + "run-eval": False, }, ] result = mark_all_eval_entries(mark_eval_entries(entries)) assert len(result) == 1 - assert result[0]['conc'] == [1, 4, 8, 16, 32] - assert result[0]['eval-all-concs'] is True - assert 'eval-conc' not in result[0] - assert result[0]['run-eval'] is True + assert result[0]["conc"] == [1, 4, 8, 16, 32] + assert result[0]["eval-all-concs"] is True + assert "eval-conc" not in result[0] + assert result[0]["run-eval"] is True def test_deduplicates_overlapping_concurrency_rows_for_same_parallelism(self): entries = [ { - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [4, 8, 16], - 'run-eval': False, - 'eval-conc': None, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [4, 8, 16], + "run-eval": False, + "eval-conc": None, }, { - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [16, 32], - 'run-eval': True, - 'eval-conc': 32, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [16, 32], + "run-eval": True, + "eval-conc": 32, }, ] result = mark_all_eval_entries(entries) assert len(result) == 1 - assert result[0]['conc'] == [4, 8, 16, 32] - assert result[0]['eval-all-concs'] is True - assert 'eval-conc' not in result[0] + assert result[0]["conc"] == [4, 8, 16, 32] + assert result[0]["eval-all-concs"] is True + assert "eval-conc" not in result[0] def test_excludes_1k1k_multinode_entries_from_expansion(self): entries = [ { # 1k1k multinode: left untouched, never batched or eval-marked - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 1024, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [4, 8, 16], - 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 1024, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [4, 8, 16], + "run-eval": False, }, { # 8k1k multinode: expanded into a batched eval row - 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', - 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', - 'prefill': {'dp-attn': False}, - 'decode': {'dp-attn': False}, - 'conc': [8, 32], - 'run-eval': False, + "model": "m", + "runner": "r", + "framework": "f", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "prefill": {"dp-attn": False}, + "decode": {"dp-attn": False}, + "conc": [8, 32], + "run-eval": False, }, ] result = mark_all_eval_entries(entries) assert len(result) == 2 - one_k = next(e for e in result if e['isl'] == 1024) - eight_k = next(e for e in result if e['isl'] == 8192) + one_k = next(e for e in result if e["isl"] == 1024) + eight_k = next(e for e in result if e["isl"] == 8192) # 1k1k untouched: not eval-marked, not batched, concurrency unchanged - assert one_k['run-eval'] is False - assert 'eval-all-concs' not in one_k - assert one_k['conc'] == [4, 8, 16] + assert one_k["run-eval"] is False + assert "eval-all-concs" not in one_k + assert one_k["conc"] == [4, 8, 16] # 8k1k expanded into a batched eval row - assert eight_k['run-eval'] is True - assert eight_k['eval-all-concs'] is True - assert eight_k['conc'] == [8, 32] + assert eight_k["run-eval"] is True + assert eight_k["eval-all-concs"] is True + assert eight_k["conc"] == [8, 32] def test_marks_agentic_entries_for_gsm8k(self): entries = [ { - 'scenario-type': 'agentic-coding', - 'model': 'm', - 'runner': 'r', - 'conc': 64, + "scenario-type": "agentic-coding", + "model": "m", + "runner": "r", + "conc": 64, } ] result = mark_all_eval_entries(entries) - assert result[0]['run-eval'] is True - assert 'eval-conc' not in result[0] + assert result[0]["run-eval"] is True + assert "eval-conc" not in result[0] def test_marks_multinode_agentic_entries_for_swebench(self): """Unlike fixed-seq-len multi-node (which batches every concurrency @@ -901,55 +1056,69 @@ def test_marks_multinode_agentic_entries_for_swebench(self): the same topology are merged but only their highest conc is marked via eval-conc, since SWE-bench doesn't support batched concurrencies.""" common = { - 'scenario-type': 'agentic-coding', - 'model': 'm', 'runner': 'r', 'framework': 'sglang-disagg', - 'precision': 'fp4', 'spec-decoding': 'none', 'disagg': True, - 'prefill': {'num-worker': 1, 'tp': 8, 'ep': 1, 'dp-attn': False}, - 'decode': {'num-worker': 1, 'tp': 8, 'ep': 1, 'dp-attn': False}, + "scenario-type": "agentic-coding", + "model": "m", + "runner": "r", + "framework": "sglang-disagg", + "precision": "fp4", + "spec-decoding": "none", + "disagg": True, + "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, } entries = [ - {**common, 'conc': [2], 'exp-name': 'p1x8_d1x8_conc2'}, - {**common, 'conc': [16], 'exp-name': 'p1x8_d1x8_conc16'}, - {**common, 'conc': [32], 'exp-name': 'p1x8_d1x8_conc32'}, + {**common, "conc": [2], "exp-name": "p1x8_d1x8_conc2"}, + {**common, "conc": [16], "exp-name": "p1x8_d1x8_conc16"}, + {**common, "conc": [32], "exp-name": "p1x8_d1x8_conc32"}, ] result = mark_all_eval_entries(entries) assert len(result) == 1 - assert result[0]['run-eval'] is True - assert result[0]['conc'] == [2, 16, 32] - assert result[0]['eval-conc'] == 32 - assert 'eval-all-concs' not in result[0] + assert result[0]["run-eval"] is True + assert result[0]["conc"] == [2, 16, 32] + assert result[0]["eval-conc"] == 32 + assert "eval-all-concs" not in result[0] # ============================================================================= # Test generate_full_sweep for single-node # ============================================================================= + class TestGenerateFullSweepSingleNode: """Tests for generate_full_sweep with single-node configs.""" - def test_basic_sweep_generation(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_basic_sweep_generation( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Basic single-node sweep should generate entries.""" result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) > 0 # With step_size=2, conc goes 4, 8, 16, 32, 64 = 5 values per seq-len config # 2 seq-len configs * 5 = 10 entries assert len(result) == 10 - def test_matrix_entry_structure(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_matrix_entry_structure( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Generated entries should have correct structure.""" result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) entry = result[0] - assert entry["image"] == "rocm/7.0:rocm7.0_ubuntu_22.04_sgl-dev-v0.5.2-rocm7.0-mi30x-20250915" + assert ( + entry["image"] + == "rocm/7.0:rocm7.0_ubuntu_22.04_sgl-dev-v0.5.2-rocm7.0-mi30x-20250915" + ) assert entry["model"] == "deepseek-ai/DeepSeek-R1-0528" assert entry["precision"] == "fp8" assert entry["framework"] == "sglang" @@ -960,7 +1129,9 @@ def test_matrix_entry_structure(self, sample_single_node_config, sample_runner_c assert (entry["pp"], entry["dcp-size"], entry["pcp-size"]) == (1, 1, 1) explicit_config = copy.deepcopy(sample_single_node_config) - for seq_config in explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"]: + for seq_config in explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"][ + "fixed-seq-len" + ]: for search_entry in seq_config["search-space"]: search_entry.update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) explicit_result = generate_full_sweep( @@ -969,141 +1140,163 @@ def test_matrix_entry_structure(self, sample_single_node_config, sample_runner_c sample_runner_config, ) assert { - (row["pp"], row["dcp-size"], row["pcp-size"]) - for row in explicit_result + (row["pp"], row["dcp-size"], row["pcp-size"]) for row in explicit_result } == {(2, 2, 2)} - def test_filter_by_model_prefix(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_filter_by_model_prefix( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Filter by model prefix should work.""" full_sweep_args_single_node.model_prefix = ["dsr1"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) > 0 # Non-matching prefix should return empty full_sweep_args_single_node.model_prefix = ["nonexistent"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) == 0 - def test_filter_by_precision(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_filter_by_precision( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Filter by precision should work.""" full_sweep_args_single_node.precision = ["fp8"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.precision = ["fp4"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) == 0 - def test_filter_by_framework(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_filter_by_framework( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Filter by framework should work.""" full_sweep_args_single_node.framework = ["sglang"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.framework = ["vllm"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) == 0 - def test_filter_by_runner_type(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_filter_by_runner_type( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Filter by runner type should work.""" full_sweep_args_single_node.runner_type = ["mi300x"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.runner_type = ["h100"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert len(result) == 0 - def test_invalid_runner_type_raises_error(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_invalid_runner_type_raises_error( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Invalid runner type should raise ValueError.""" full_sweep_args_single_node.runner_type = ["invalid_runner"] with pytest.raises(ValueError) as exc_info: generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config + sample_runner_config, ) assert "Invalid runner type" in str(exc_info.value) - def test_filter_by_seq_lens(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_filter_by_seq_lens( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Filter by sequence lengths should work.""" full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # Only 1k1k entries, 5 concurrency values assert len(result) == 5 assert all(entry["isl"] == 1024 and entry["osl"] == 1024 for entry in result) - def test_max_conc_filter(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_conc_filter( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """max_conc filter should limit concurrency values.""" full_sweep_args_single_node.max_conc = 16 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # conc values: 4, 8, 16 (32, 64 filtered out) assert len(result) == 3 assert all(entry["conc"] <= 16 for entry in result) - def test_max_conc_creates_config_when_below_min(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_conc_creates_config_when_below_min( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """max_conc below config's min should create config with max_conc value.""" # Config has conc-start=4, so max_conc=1 should create entry with conc=1 full_sweep_args_single_node.max_conc = 1 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # Should create 1 entry with conc=1 assert len(result) == 1 assert result[0]["conc"] == 1 - def test_max_conc_zero_or_negative_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_conc_zero_or_negative_skips( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """max_conc of 0 or negative should skip configs.""" for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_conc = invalid_value result = generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config + sample_runner_config, ) assert len(result) == 0, f"Expected 0 results for max_conc={invalid_value}" @@ -1120,13 +1313,20 @@ def test_max_tp_filter(self, sample_runner_config, full_sweep_args_single_node): "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ - {"tp": 4, "conc-start": 4, "conc-end": 64}, # should remain - {"tp": 8, "conc-start": 4, "conc-end": 64}, # should be skipped + { + "tp": 4, + "conc-start": 4, + "conc-end": 64, + }, # should remain + { + "tp": 8, + "conc-start": 4, + "conc-end": 64, + }, # should be skipped ], } ] @@ -1147,7 +1347,12 @@ def test_max_tp_filter(self, sample_runner_config, full_sweep_args_single_node): assert len(result) == 5 assert all(entry["tp"] == 4 for entry in result) - def test_max_tp_below_all_available_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_tp_below_all_available_skips( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """If all available tp values are > max_tp, generator should return empty (skip).""" full_sweep_args_single_node.max_tp = 2 full_sweep_args_single_node.seq_lens = ["1k1k"] @@ -1160,25 +1365,33 @@ def test_max_tp_below_all_available_skips(self, sample_single_node_config, sampl assert len(result) == 0 - def test_max_tp_zero_or_negative_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_tp_zero_or_negative_skips( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """max_tp of 0 or negative should skip configs.""" for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_tp = invalid_value result = generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config + sample_runner_config, ) assert len(result) == 0, f"Expected 0 results for max_tp={invalid_value}" - def test_step_size(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_step_size( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Different step sizes should affect concurrency progression.""" full_sweep_args_single_node.step_size = 4 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # conc: 4, 16, 64 = 3 values assert len(result) == 3 @@ -1187,37 +1400,48 @@ def test_step_size(self, sample_single_node_config, sample_runner_config, full_s assert 16 in conc_values assert 64 in conc_values - def test_exp_name_format(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_exp_name_format( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """exp-name should have correct format.""" full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) assert all(entry["exp-name"] == "dsr1_1k1k" for entry in result) - def test_max_model_len_calculation(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_max_model_len_calculation( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """max-model-len should be isl + osl + 256.""" result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) for entry in result: expected_max_model_len = entry["isl"] + entry["osl"] + 256 assert entry["max-model-len"] == expected_max_model_len - def test_runner_node_filter(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_runner_node_filter( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Runner node filter should expand entries to individual matching nodes.""" full_sweep_args_single_node.runner_type = ["mi300x"] full_sweep_args_single_node.runner_node_filter = "amd" full_sweep_args_single_node.seq_lens = ["1k1k"] - full_sweep_args_single_node.max_conc = 4 # Limit to single conc value for easier counting + full_sweep_args_single_node.max_conc = ( + 4 # Limit to single conc value for easier counting + ) result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # 2 amd nodes (mi300x-amd_0, mi300x-amd_1), 1 conc value = 2 entries assert len(result) == 2 @@ -1226,56 +1450,62 @@ def test_runner_node_filter(self, sample_single_node_config, sample_runner_confi assert "mi300x-amd_0" in runners assert "mi300x-amd_1" in runners - def test_runner_node_filter_no_match(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_runner_node_filter_no_match( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Runner node filter with no matches should skip configs (return empty).""" full_sweep_args_single_node.runner_type = ["mi300x"] full_sweep_args_single_node.runner_node_filter = "nonexistent" result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # No nodes match, so config is skipped assert len(result) == 0 - def test_runner_node_filter_without_runner_type(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): + def test_runner_node_filter_without_runner_type( + self, + sample_single_node_config, + sample_runner_config, + full_sweep_args_single_node, + ): """Runner node filter should work without explicit runner type (uses config's runner).""" full_sweep_args_single_node.runner_node_filter = "amd" full_sweep_args_single_node.seq_lens = ["1k1k"] full_sweep_args_single_node.max_conc = 4 result = generate_full_sweep( - full_sweep_args_single_node, - sample_single_node_config, - sample_runner_config + full_sweep_args_single_node, sample_single_node_config, sample_runner_config ) # Config has runner=mi300x, filter "amd" matches mi300x-amd_0 and mi300x-amd_1 assert len(result) == 2 assert all("amd" in entry["runner"] for entry in result) - # ============================================================================= # Test generate_full_sweep for multi-node # ============================================================================= + class TestGenerateFullSweepMultiNode: """Tests for generate_full_sweep with multi-node configs.""" - def test_multinode_sweep_generation(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_sweep_generation( + self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node + ): """Multinode sweep should generate entries with prefill/decode.""" result = generate_full_sweep( - full_sweep_args_multi_node, - sample_multinode_config, - sample_runner_config + full_sweep_args_multi_node, sample_multinode_config, sample_runner_config ) assert len(result) == 1 # One entry with conc-list - def test_multinode_entry_structure(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_entry_structure( + self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node + ): """Multinode entries should have prefill and decode configs.""" result = generate_full_sweep( - full_sweep_args_multi_node, - sample_multinode_config, - sample_runner_config + full_sweep_args_multi_node, sample_multinode_config, sample_runner_config ) entry = result[0] assert "prefill" in entry @@ -1296,9 +1526,13 @@ def test_multinode_entry_structure(self, sample_multinode_config, sample_runner_ entry["decode"]["pcp-size"], ) == (1, 1, 1) - def test_multinode_parallelism_fields(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_parallelism_fields( + self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node + ): explicit_config = copy.deepcopy(sample_multinode_config) - search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0]["search-space"][0] + search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"][ + "fixed-seq-len" + ][0]["search-space"][0] search_entry["prefill"].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) search_entry["decode"].update({"pp": 2, "dcp-size": 4, "pcp-size": 1}) @@ -1319,27 +1553,29 @@ def test_multinode_parallelism_fields(self, sample_multinode_config, sample_runn entry["decode"]["pcp-size"], ) == (2, 4, 1) - def test_multinode_conc_as_list(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_conc_as_list( + self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node + ): """Multinode conc should be passed as list.""" result = generate_full_sweep( - full_sweep_args_multi_node, - sample_multinode_config, - sample_runner_config + full_sweep_args_multi_node, sample_multinode_config, sample_runner_config ) entry = result[0] assert isinstance(entry["conc"], list) assert entry["conc"] == [2150] - def test_single_node_flag_skips_multinode(self, sample_multinode_config, sample_runner_config, full_sweep_args_single_node): + def test_single_node_flag_skips_multinode( + self, sample_multinode_config, sample_runner_config, full_sweep_args_single_node + ): """Single-node flag should skip multinode configs.""" result = generate_full_sweep( - full_sweep_args_single_node, - sample_multinode_config, - sample_runner_config + full_sweep_args_single_node, sample_multinode_config, sample_runner_config ) assert len(result) == 0 - def test_runner_node_filter_multinode(self, sample_runner_config, full_sweep_args_multi_node): + def test_runner_node_filter_multinode( + self, sample_runner_config, full_sweep_args_multi_node + ): """Runner node filter should work with multinode configs.""" # Create a multinode config with h200 runner (which has 4 nodes) config = { @@ -1355,7 +1591,6 @@ def test_runner_node_filter_multinode(self, sample_runner_config, full_sweep_arg "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, @@ -1375,18 +1610,16 @@ def test_runner_node_filter_multinode(self, sample_runner_config, full_sweep_arg "dp-attn": False, }, } - ] + ], } ] - } + }, } } full_sweep_args_multi_node.runner_type = ["h200"] full_sweep_args_multi_node.runner_node_filter = "cw" result = generate_full_sweep( - full_sweep_args_multi_node, - config, - sample_runner_config + full_sweep_args_multi_node, config, sample_runner_config ) # Only h200-cw_0 and h200-cw_1 match "cw" filter assert len(result) == 2 @@ -1400,10 +1633,13 @@ def test_runner_node_filter_multinode(self, sample_runner_config, full_sweep_arg # Test edge cases and special configurations # ============================================================================= + class TestEdgeCases: """Tests for edge cases and special configurations.""" - def test_config_with_ep_and_dp_attn(self, sample_runner_config, full_sweep_args_single_node): + def test_config_with_ep_and_dp_attn( + self, sample_runner_config, full_sweep_args_single_node + ): """Config with ep and dp-attn should be handled correctly.""" config = { "test-config": { @@ -1416,28 +1652,33 @@ def test_config_with_ep_and_dp_attn(self, sample_runner_config, full_sweep_args_ "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ - {"tp": 4, "ep": 4, "dp-attn": True, "conc-start": 4, "conc-end": 4} - ] + { + "tp": 4, + "ep": 4, + "dp-attn": True, + "conc-start": 4, + "conc-end": 4, + } + ], } ] - } + }, } } result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) assert len(result) == 1 assert result[0]["ep"] == 4 assert result[0]["dp-attn"] is True - def test_config_with_spec_decoding(self, sample_runner_config, full_sweep_args_single_node): + def test_config_with_spec_decoding( + self, sample_runner_config, full_sweep_args_single_node + ): """Config with spec-decoding should be handled correctly.""" config = { "test-config": { @@ -1450,27 +1691,31 @@ def test_config_with_spec_decoding(self, sample_runner_config, full_sweep_args_s "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ - {"tp": 8, "spec-decoding": "mtp", "conc-start": 4, "conc-end": 4} - ] + { + "tp": 8, + "spec-decoding": "mtp", + "conc-start": 4, + "conc-end": 4, + } + ], } ] - } + }, } } result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) assert len(result) == 1 assert result[0]["spec-decoding"] == "mtp" - def test_conc_list_in_single_node(self, sample_runner_config, full_sweep_args_single_node): + def test_conc_list_in_single_node( + self, sample_runner_config, full_sweep_args_single_node + ): """Single node config with conc-list should work.""" config = { "test-config": { @@ -1483,22 +1728,17 @@ def test_conc_list_in_single_node(self, sample_runner_config, full_sweep_args_si "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, - "search-space": [ - {"tp": 8, "conc-list": [4, 16, 64]} - ] + "search-space": [{"tp": 8, "conc-list": [4, 16, 64]}], } ] - } + }, } } result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) conc_values = [entry["conc"] for entry in result] assert conc_values == [4, 16, 64] @@ -1522,9 +1762,7 @@ def test_conc_list_in_single_node_honors_filters( { "isl": 1024, "osl": 1024, - "search-space": [ - {"tp": 8, "conc-list": [4, 16, 64]} - ], + "search-space": [{"tp": 8, "conc-list": [4, 16, 64]}], } ] }, @@ -1572,7 +1810,9 @@ def test_min_conc_cannot_exceed_max_conc( sample_runner_config, ) - def test_disagg_defaults_to_false(self, sample_runner_config, full_sweep_args_single_node): + def test_disagg_defaults_to_false( + self, sample_runner_config, full_sweep_args_single_node + ): """disagg should default to False when not specified.""" config = { "test-config": { @@ -1586,26 +1826,23 @@ def test_disagg_defaults_to_false(self, sample_runner_config, full_sweep_args_si # No disagg field "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, - "search-space": [ - {"tp": 8, "conc-start": 4, "conc-end": 4} - ] + "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 4}], } ] - } + }, } } result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) assert result[0]["disagg"] is False - def test_multinode_conc_range_expansion(self, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_conc_range_expansion( + self, sample_runner_config, full_sweep_args_multi_node + ): """Multinode with conc range should expand to list.""" config = { "test-config": { @@ -1620,7 +1857,6 @@ def test_multinode_conc_range_expansion(self, sample_runner_config, full_sweep_a "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, @@ -1641,22 +1877,22 @@ def test_multinode_conc_range_expansion(self, sample_runner_config, full_sweep_a "dp-attn": False, }, } - ] + ], } ] - } + }, } } result = generate_full_sweep( - full_sweep_args_multi_node, - config, - sample_runner_config + full_sweep_args_multi_node, config, sample_runner_config ) assert len(result) == 1 # step_size=2: 1, 2, 4, 8 assert result[0]["conc"] == [1, 2, 4, 8] - def test_max_ep_creates_config_when_below_min(self, sample_runner_config, full_sweep_args_single_node): + def test_max_ep_creates_config_when_below_min( + self, sample_runner_config, full_sweep_args_single_node + ): """max_ep below config's ep should create config with max_ep value.""" config = { "test-config": { @@ -1669,29 +1905,28 @@ def test_max_ep_creates_config_when_below_min(self, sample_runner_config, full_s "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ {"tp": 8, "ep": 8, "conc-start": 4, "conc-end": 4} - ] + ], } ] - } + }, } } full_sweep_args_single_node.max_ep = 2 result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) # ep=8 in config, but max_ep=2, so should use ep=2 assert len(result) == 1 assert result[0]["ep"] == 2 - def test_max_ep_zero_or_negative_skips(self, sample_runner_config, full_sweep_args_single_node): + def test_max_ep_zero_or_negative_skips( + self, sample_runner_config, full_sweep_args_single_node + ): """max_ep of 0 or negative should skip configs.""" config = { "test-config": { @@ -1704,28 +1939,27 @@ def test_max_ep_zero_or_negative_skips(self, sample_runner_config, full_sweep_ar "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ {"tp": 8, "ep": 8, "conc-start": 4, "conc-end": 4} - ] + ], } ] - } + }, } } for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_ep = invalid_value result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_ep={invalid_value}" - def test_multinode_max_conc_zero_or_negative_skips(self, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_max_conc_zero_or_negative_skips( + self, sample_runner_config, full_sweep_args_multi_node + ): """Multinode max_conc of 0 or negative should skip configs.""" config = { "test-config": { @@ -1740,7 +1974,6 @@ def test_multinode_max_conc_zero_or_negative_skips(self, sample_runner_config, f "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, @@ -1760,22 +1993,22 @@ def test_multinode_max_conc_zero_or_negative_skips(self, sample_runner_config, f "dp-attn": False, }, } - ] + ], } ] - } + }, } } for invalid_value in [0, -1, -100]: full_sweep_args_multi_node.max_conc = invalid_value result = generate_full_sweep( - full_sweep_args_multi_node, - config, - sample_runner_config + full_sweep_args_multi_node, config, sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_conc={invalid_value}" - def test_multinode_max_conc_creates_config_when_below_min(self, sample_runner_config, full_sweep_args_multi_node): + def test_multinode_max_conc_creates_config_when_below_min( + self, sample_runner_config, full_sweep_args_multi_node + ): """Multinode max_conc below all values should create config with max_conc.""" config = { "test-config": { @@ -1790,7 +2023,6 @@ def test_multinode_max_conc_creates_config_when_below_min(self, sample_runner_co "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, @@ -1810,23 +2042,23 @@ def test_multinode_max_conc_creates_config_when_below_min(self, sample_runner_co "dp-attn": False, }, } - ] + ], } ] - } + }, } } full_sweep_args_multi_node.max_conc = 1 result = generate_full_sweep( - full_sweep_args_multi_node, - config, - sample_runner_config + full_sweep_args_multi_node, config, sample_runner_config ) # All conc values (100, 200, 400) > max_conc (1), so should use [1] assert len(result) == 1 assert result[0]["conc"] == [1] - def test_combined_max_filters(self, sample_runner_config, full_sweep_args_single_node): + def test_combined_max_filters( + self, sample_runner_config, full_sweep_args_single_node + ): """Multiple max filters should all apply (tp skip, ep clamp, conc clamp).""" config = { "test-config": { @@ -1839,17 +2071,26 @@ def test_combined_max_filters(self, sample_runner_config, full_sweep_args_single "multinode": False, "scenarios": { "fixed-seq-len": [ - { "isl": 1024, "osl": 1024, "search-space": [ - {"tp": 8, "ep": 8, "conc-start": 100, "conc-end": 200}, # should be skipped - {"tp": 2, "ep": 8, "conc-start": 100, "conc-end": 200}, # should remain - ] + { + "tp": 8, + "ep": 8, + "conc-start": 100, + "conc-end": 200, + }, # should be skipped + { + "tp": 2, + "ep": 8, + "conc-start": 100, + "conc-end": 200, + }, # should remain + ], } ] - } + }, } } full_sweep_args_single_node.max_tp = 2 @@ -1857,9 +2098,7 @@ def test_combined_max_filters(self, sample_runner_config, full_sweep_args_single full_sweep_args_single_node.max_conc = 1 result = generate_full_sweep( - full_sweep_args_single_node, - config, - sample_runner_config + full_sweep_args_single_node, config, sample_runner_config ) assert len(result) == 1 @@ -1867,10 +2106,12 @@ def test_combined_max_filters(self, sample_runner_config, full_sweep_args_single assert result[0]["ep"] == 1 assert result[0]["conc"] == 1 + # ============================================================================= # Test argument parsing and defaults # ============================================================================= + class TestArgumentDefaults: """Tests for command-line argument parsing and default values.""" @@ -1885,10 +2126,11 @@ def test_runner_config_default_value(self): try: # Simulate command-line args without --runner-config flag sys.argv = [ - 'generate_sweep_configs.py', - 'full-sweep', - '--config-files', 'dummy.yaml', - '--single-node' + "generate_sweep_configs.py", + "full-sweep", + "--config-files", + "dummy.yaml", + "--single-node", ] # Parse args using the ArgumentParser from main @@ -1899,44 +2141,44 @@ def test_runner_config_default_value(self): # Create the same parent parser as in main() parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - '--config-files', - nargs='+', + "--config-files", + nargs="+", required=True, - help='One or more configuration files (YAML format)' + help="One or more configuration files (YAML format)", ) parent_parser.add_argument( - '--runner-config', - default='configs/runners.yaml', - help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' + "--runner-config", + default="configs/runners.yaml", + help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", ) # Create main parser parser = argparse.ArgumentParser( - description='Generate benchmark configurations from YAML config files' + description="Generate benchmark configurations from YAML config files" ) # Create subparsers subparsers = parser.add_subparsers( - dest='command', - required=True, - help='Available commands' + dest="command", required=True, help="Available commands" ) # Add full-sweep subparser full_sweep_parser = subparsers.add_parser( - 'full-sweep', + "full-sweep", parents=[parent_parser], add_help=False, - help='Generate full sweep configurations' + help="Generate full sweep configurations", ) - full_sweep_parser.add_argument('--single-node', action='store_true') - full_sweep_parser.add_argument('--multi-node', action='store_true') + full_sweep_parser.add_argument("--single-node", action="store_true") + full_sweep_parser.add_argument("--multi-node", action="store_true") # Parse the args - args = parser.parse_args(['full-sweep', '--config-files', 'dummy.yaml', '--single-node']) + args = parser.parse_args( + ["full-sweep", "--config-files", "dummy.yaml", "--single-node"] + ) # Verify the default value - assert args.runner_config == 'configs/runners.yaml' + assert args.runner_config == "configs/runners.yaml" finally: # Restore original sys.argv @@ -1949,48 +2191,50 @@ def test_runner_config_explicit_value(self): # Create the same parent parser as in main() parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - '--config-files', - nargs='+', + "--config-files", + nargs="+", required=True, - help='One or more configuration files (YAML format)' + help="One or more configuration files (YAML format)", ) parent_parser.add_argument( - '--runner-config', - default='configs/runners.yaml', - help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' + "--runner-config", + default="configs/runners.yaml", + help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", ) # Create main parser parser = argparse.ArgumentParser( - description='Generate benchmark configurations from YAML config files' + description="Generate benchmark configurations from YAML config files" ) # Create subparsers subparsers = parser.add_subparsers( - dest='command', - required=True, - help='Available commands' + dest="command", required=True, help="Available commands" ) # Add full-sweep subparser full_sweep_parser = subparsers.add_parser( - 'full-sweep', + "full-sweep", parents=[parent_parser], add_help=False, - help='Generate full sweep configurations' + help="Generate full sweep configurations", ) - full_sweep_parser.add_argument('--single-node', action='store_true') + full_sweep_parser.add_argument("--single-node", action="store_true") # Parse with explicit --runner-config - args = parser.parse_args([ - 'full-sweep', - '--config-files', 'dummy.yaml', - '--runner-config', 'custom/path/runners.yaml', - '--single-node' - ]) + args = parser.parse_args( + [ + "full-sweep", + "--config-files", + "dummy.yaml", + "--runner-config", + "custom/path/runners.yaml", + "--single-node", + ] + ) # Verify the explicit value - assert args.runner_config == 'custom/path/runners.yaml' + assert args.runner_config == "custom/path/runners.yaml" def test_all_evals_cli_marks_every_fixed_sequence_entry( self, @@ -2005,33 +2249,39 @@ def test_all_evals_cli_marks_every_fixed_sequence_entry( monkeypatch.setattr( generate_sweep_configs, - 'load_config_files', + "load_config_files", lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - 'load_runner_file', + "load_runner_file", lambda _: sample_runner_config, ) - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'test-config', - '--config-files', 'dummy.yaml', - '--config-keys', 'dsr1-fp8-mi300x-sglang', - '--all-evals', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "test-config", + "--config-files", + "dummy.yaml", + "--config-keys", + "dsr1-fp8-mi300x-sglang", + "--all-evals", + ], + ) result = generate_sweep_configs.main() # Every 8k1k concurrency is marked (5 conc values), and the 1k1k # entries are dropped rather than evaluated. assert len(result) == 5 - assert {(entry['isl'], entry['osl']) for entry in result} == { + assert {(entry["isl"], entry["osl"]) for entry in result} == { (8192, 1024), } - assert min(entry['conc'] for entry in result) == 4 - assert all(entry['run-eval'] is True for entry in result) - assert all(entry['eval-only'] is True for entry in result) + assert min(entry["conc"] for entry in result) == 4 + assert all(entry["run-eval"] is True for entry in result) + assert all(entry["eval-only"] is True for entry in result) def test_all_evals_composes_with_evals_only( self, @@ -2044,31 +2294,37 @@ def test_all_evals_composes_with_evals_only( monkeypatch.setattr( generate_sweep_configs, - 'load_config_files', + "load_config_files", lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - 'load_runner_file', + "load_runner_file", lambda _: sample_runner_config, ) - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'test-config', - '--config-files', 'dummy.yaml', - '--config-keys', 'dsr1-fp8-mi300x-sglang', - '--evals-only', - '--all-evals', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "test-config", + "--config-files", + "dummy.yaml", + "--config-keys", + "dsr1-fp8-mi300x-sglang", + "--evals-only", + "--all-evals", + ], + ) result = generate_sweep_configs.main() assert len(result) == 5 - assert {(entry['isl'], entry['osl']) for entry in result} == { + assert {(entry["isl"], entry["osl"]) for entry in result} == { (8192, 1024), } - assert all(entry['run-eval'] is True for entry in result) - assert all(entry['eval-only'] is True for entry in result) + assert all(entry["run-eval"] is True for entry in result) + assert all(entry["eval-only"] is True for entry in result) def test_trim_conc_reduces_generated_eval_matrix( self, @@ -2081,50 +2337,56 @@ def test_trim_conc_reduces_generated_eval_matrix( monkeypatch.setattr( generate_sweep_configs, - 'load_config_files', + "load_config_files", lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - 'load_runner_file', + "load_runner_file", lambda _: sample_runner_config, ) - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'test-config', - '--config-files', 'dummy.yaml', - '--config-keys', 'dsr1-fp8-mi300x-sglang', - '--evals-only', - '--all-evals', - '--trim-conc', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "test-config", + "--config-files", + "dummy.yaml", + "--config-keys", + "dsr1-fp8-mi300x-sglang", + "--evals-only", + "--all-evals", + "--trim-conc", + ], + ) result = generate_sweep_configs.main() assert len(result) == 1 - assert result[0]['conc'] == 4 - assert result[0]['run-eval'] is True - assert result[0]['eval-only'] is True + assert result[0]["conc"] == 4 + assert result[0]["run-eval"] is True + assert result[0]["eval-only"] is True def test_trim_conc_updates_multinode_dispatch_concurrency(self): low_entry = { - 'prefill': {'num-worker': 1, 'tp': 8}, - 'decode': {'num-worker': 0, 'tp': 8}, - 'conc': [4], + "prefill": {"num-worker": 1, "tp": 8}, + "decode": {"num-worker": 0, "tp": 8}, + "conc": [4], } high_entry = { **low_entry, - 'conc': [64], - 'run-eval': True, - 'eval-conc': 64, + "conc": [64], + "run-eval": True, + "eval-conc": 64, } result = trim_conc([high_entry, low_entry]) assert len(result) == 1 - assert result[0]['conc'] == [4] - assert result[0]['eval-conc'] == 4 - assert result[0]['run-eval'] is True + assert result[0]["conc"] == [4] + assert result[0]["eval-conc"] == 4 + assert result[0]["run-eval"] is True def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( self, @@ -2134,43 +2396,47 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( import generate_sweep_configs repo_root = Path(__file__).resolve().parents[2] - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'full-sweep', - '--config-files', - str(repo_root / 'configs/nvidia-master.yaml'), - str(repo_root / 'configs/amd-master.yaml'), - '--runner-config', - str(repo_root / 'configs/runners.yaml'), - '--model-prefix', - 'kimik3', - 'minimaxm3', - '--scenario-type', - 'agentic-coding', - '--evals-only', - '--all-evals', - '--trim-conc', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "full-sweep", + "--config-files", + str(repo_root / "configs/nvidia-master.yaml"), + str(repo_root / "configs/amd-master.yaml"), + "--runner-config", + str(repo_root / "configs/runners.yaml"), + "--model-prefix", + "kimik3", + "minimaxm3", + "--scenario-type", + "agentic-coding", + "--evals-only", + "--all-evals", + "--trim-conc", + ], + ) rows = generate_sweep_configs.main() manifest_fields = ( - 'model-prefix', - 'runner', - 'framework', - 'precision', - 'tp', - 'pp', - 'dcp-size', - 'pcp-size', - 'ep', - 'dp-attn', - 'prefill', - 'decode', - 'disagg', - 'kv-offloading', - 'kv-offload-backend', - 'spec-decoding', - 'exp-name', + "model-prefix", + "runner", + "framework", + "precision", + "tp", + "pp", + "dcp-size", + "pcp-size", + "ep", + "dp-attn", + "prefill", + "decode", + "disagg", + "kv-offloading", + "kv-offload-backend", + "spec-decoding", + "exp-name", ) manifest = sorted( tuple( @@ -2183,18 +2449,18 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( for row in rows ) manifest_digest = hashlib.sha256( - json.dumps(manifest, separators=(',', ':')).encode() + json.dumps(manifest, separators=(",", ":")).encode() ).hexdigest() assert len(manifest) == 67 assert manifest_digest == ( - 'b94441edd3d083d3c03a6ed1f3891ee222c420614ba6a2a68802d9a85c128ca2' + "b94441edd3d083d3c03a6ed1f3891ee222c420614ba6a2a68802d9a85c128ca2" ), json.dumps(manifest, indent=2) for row in rows: - if isinstance(row['conc'], list): - assert row['conc'] == [row['eval-conc']] - assert all(row['run-eval'] is True for row in rows) - assert all(row['eval-only'] is True for row in rows) + if isinstance(row["conc"], list): + assert row["conc"] == [row["eval-conc"]] + assert all(row["run-eval"] is True for row in rows) + assert all(row["eval-only"] is True for row in rows) def test_all_evals_batches_each_multinode_concurrency( self, @@ -2206,55 +2472,64 @@ def test_all_evals_batches_each_multinode_concurrency( import generate_sweep_configs config = sample_multinode_config - seq_entry = ( - config['dsr1-fp4-gb200-dynamo-trt']['scenarios'] - ['fixed-seq-len'][0] - ) + seq_entry = config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0] # all-evals only evaluates 8k1k, so target that sequence length. - seq_entry['isl'] = 8192 - seq_entry['osl'] = 1024 - search_space = seq_entry['search-space'] - search_space[0]['conc-list'] = [4, 16, 64] + seq_entry["isl"] = 8192 + seq_entry["osl"] = 1024 + search_space = seq_entry["search-space"] + search_space[0]["conc-list"] = [4, 16, 64] monkeypatch.setattr( generate_sweep_configs, - 'load_config_files', + "load_config_files", lambda _: config, ) monkeypatch.setattr( generate_sweep_configs, - 'load_runner_file', + "load_runner_file", lambda _: sample_runner_config, ) - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'test-config', - '--config-files', 'dummy.yaml', - '--config-keys', 'dsr1-fp4-gb200-dynamo-trt', - '--all-evals', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "test-config", + "--config-files", + "dummy.yaml", + "--config-keys", + "dsr1-fp4-gb200-dynamo-trt", + "--all-evals", + ], + ) result = generate_sweep_configs.main() assert len(result) == 1 - assert result[0]['conc'] == [4, 16, 64] - assert result[0]['eval-all-concs'] is True - assert 'eval-conc' not in result[0] - assert all(entry['run-eval'] is True for entry in result) - assert all(entry['eval-only'] is True for entry in result) + assert result[0]["conc"] == [4, 16, 64] + assert result[0]["eval-all-concs"] is True + assert "eval-conc" not in result[0] + assert all(entry["run-eval"] is True for entry in result) + assert all(entry["eval-only"] is True for entry in result) def test_all_evals_cannot_combine_with_no_evals(self, monkeypatch): import sys import generate_sweep_configs - monkeypatch.setattr(sys, 'argv', [ - 'generate_sweep_configs.py', - 'test-config', - '--config-files', 'dummy.yaml', - '--config-keys', 'dummy', - '--no-evals', - '--all-evals', - ]) + monkeypatch.setattr( + sys, + "argv", + [ + "generate_sweep_configs.py", + "test-config", + "--config-files", + "dummy.yaml", + "--config-keys", + "dummy", + "--no-evals", + "--all-evals", + ], + ) with pytest.raises(SystemExit): generate_sweep_configs.main() @@ -2264,6 +2539,7 @@ def test_all_evals_cannot_combine_with_no_evals(self, monkeypatch): # Mixed-mode fixtures # ============================================================================= + @pytest.fixture def sample_mixed_config(sample_single_node_config, sample_multinode_config): """Config dict containing both single-node and multinode entries.""" @@ -2297,6 +2573,7 @@ def full_sweep_args_both(): # Test generate_test_config_sweep # ============================================================================= + class TestGenerateTestConfigSweep: """Tests for exact config-key sweep generation.""" @@ -2316,20 +2593,18 @@ def test_single_node_parallelism_fields_are_generated( args, sample_single_node_config, sample_runner_config ) assert [ - (row["pp"], row["dcp-size"], row["pcp-size"]) - for row in default_result + (row["pp"], row["dcp-size"], row["pcp-size"]) for row in default_result ] == [(1, 1, 1)] explicit_config = copy.deepcopy(sample_single_node_config) - explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"][0]["search-space"][0].update( - {"pp": 2, "dcp-size": 2, "pcp-size": 2} - ) + explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"][0][ + "search-space" + ][0].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) explicit_result = generate_test_config_sweep( args, explicit_config, sample_runner_config ) assert [ - (row["pp"], row["dcp-size"], row["pcp-size"]) - for row in explicit_result + (row["pp"], row["dcp-size"], row["pcp-size"]) for row in explicit_result ] == [(2, 2, 2)] def test_multinode_parallelism_fields_are_generated( @@ -2344,13 +2619,15 @@ def test_multinode_parallelism_fields_are_generated( runner_node_filter=None, ) explicit_config = copy.deepcopy(sample_multinode_config) - search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0]["search-space"][0] + search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"][ + "fixed-seq-len" + ][0]["search-space"][0] search_entry["prefill"].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) search_entry["decode"].update({"pp": 2, "dcp-size": 4, "pcp-size": 1}) - entry = generate_test_config_sweep( - args, explicit_config, sample_runner_config - )[0] + entry = generate_test_config_sweep(args, explicit_config, sample_runner_config)[ + 0 + ] assert ( entry["prefill"]["pp"], @@ -2363,7 +2640,9 @@ def test_multinode_parallelism_fields_are_generated( entry["decode"]["pcp-size"], ) == (2, 4, 1) - def test_runner_node_filter_expands_config_runner(self, sample_multinode_config, sample_runner_config): + def test_runner_node_filter_expands_config_runner( + self, sample_multinode_config, sample_runner_config + ): """test-config should allow targeting one concrete runner node.""" args = argparse.Namespace( config_keys=["dsr1-fp4-gb200-dynamo-trt"], @@ -2381,7 +2660,9 @@ def test_runner_node_filter_expands_config_runner(self, sample_multinode_config, assert len(result) == 1 assert result[0]["runner"] == "gb200-nv_0" - def test_runner_node_filter_no_match_skips_config(self, sample_multinode_config, sample_runner_config): + def test_runner_node_filter_no_match_skips_config( + self, sample_multinode_config, sample_runner_config + ): """Unmatched node filters should produce no entries.""" args = argparse.Namespace( config_keys=["dsr1-fp4-gb200-dynamo-trt"], @@ -2398,7 +2679,9 @@ def test_runner_node_filter_no_match_skips_config(self, sample_multinode_config, assert result == [] - def test_runner_node_filter_expands_agentic_config_runner(self, sample_runner_config): + def test_runner_node_filter_expands_agentic_config_runner( + self, sample_runner_config + ): """Agentic test-config entries should support concrete runner targeting.""" config = { "qwen-agentic-hicache": { @@ -2454,40 +2737,42 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [{ - "dram-utilization": 0.80, - "search-space": [ - { - "tp": 4, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "dcp-size": 2, - "pcp-size": 1, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "dcp-size": 1, - "pcp-size": 2, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "pp": 2, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - ], - }], + "agentic-coding": [ + { + "dram-utilization": 0.80, + "search-space": [ + { + "tp": 4, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "dcp-size": 2, + "pcp-size": 1, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "dcp-size": 1, + "pcp-size": 2, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "pp": 2, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + ], + } + ], }, }, } @@ -2502,7 +2787,9 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): result = generate_test_config_sweep(args, config, sample_runner_config) budgets = { - (entry["pp"], entry["dcp-size"], entry["pcp-size"]): entry["total-cpu-dram-gb"] + (entry["pp"], entry["dcp-size"], entry["pcp-size"]): entry[ + "total-cpu-dram-gb" + ] for entry in result } assert budgets == { @@ -2524,17 +2811,19 @@ def test_agentic_node_dram_rejects_tp_above_runner_gpus(self, sample_runner_conf "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [{ - "dram-utilization": 0.80, - "search-space": [ - { - "tp": 4, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - ], - }], + "agentic-coding": [ + { + "dram-utilization": 0.80, + "search-space": [ + { + "tp": 4, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + ], + } + ], }, }, } @@ -2572,8 +2861,26 @@ def test_multinode_agentic_groups_concurrencies_per_search_entry( "search-space": [ { "conc-list": [16, 32, 64, 128, 256], - "prefill": {"hardware": "gb200", "num-worker": 2, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 2, "ep": 4, "dp-attn": False}, - "decode": {"hardware": "h100", "num-worker": 1, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 1, "ep": 1, "dp-attn": False}, + "prefill": { + "hardware": "gb200", + "num-worker": 2, + "tp": 4, + "pp": 2, + "dcp-size": 2, + "pcp-size": 2, + "ep": 4, + "dp-attn": False, + }, + "decode": { + "hardware": "h100", + "num-worker": 1, + "tp": 4, + "pp": 2, + "dcp-size": 2, + "pcp-size": 1, + "ep": 1, + "dp-attn": False, + }, } ], } @@ -2621,16 +2928,30 @@ def test_multinode_agentic_preserves_kv_offload_fields(self, sample_runner_confi "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [{ - "dram-utilization": 0.80, - "search-space": [{ - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, - "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, - }], - }], + "agentic-coding": [ + { + "dram-utilization": 0.80, + "search-space": [ + { + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + "prefill": { + "num-worker": 1, + "tp": 8, + "ep": 1, + "dp-attn": False, + }, + "decode": { + "num-worker": 1, + "tp": 8, + "ep": 1, + "dp-attn": False, + }, + } + ], + } + ], }, }, } @@ -2669,17 +2990,31 @@ def test_multinode_agentic_budget_ignores_decode_topology( "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [{ - "dram-utilization": 0.80, - "search-space": [{ - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - # prefill fills the node (8 GPUs); decode uses half. - "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, - "decode": {"num-worker": 1, "tp": 4, "ep": 1, "dp-attn": False}, - }], - }], + "agentic-coding": [ + { + "dram-utilization": 0.80, + "search-space": [ + { + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + # prefill fills the node (8 GPUs); decode uses half. + "prefill": { + "num-worker": 1, + "tp": 8, + "ep": 1, + "dp-attn": False, + }, + "decode": { + "num-worker": 1, + "tp": 4, + "ep": 1, + "dp-attn": False, + }, + } + ], + } + ], }, }, } @@ -2713,17 +3048,31 @@ def test_multinode_agentic_rejects_node_misaligned_prefill( "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [{ - "dram-utilization": 0.80, - "search-space": [{ - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - # tp=6 does not divide an 8-GPU node evenly. - "prefill": {"num-worker": 1, "tp": 6, "ep": 1, "dp-attn": False}, - "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, - }], - }], + "agentic-coding": [ + { + "dram-utilization": 0.80, + "search-space": [ + { + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + # tp=6 does not divide an 8-GPU node evenly. + "prefill": { + "num-worker": 1, + "tp": 6, + "ep": 1, + "dp-attn": False, + }, + "decode": { + "num-worker": 1, + "tp": 8, + "ep": 1, + "dp-attn": False, + }, + } + ], + } + ], }, }, } @@ -2743,6 +3092,7 @@ def test_multinode_agentic_rejects_node_misaligned_prefill( # Test apply_node_type_defaults # ============================================================================= + class TestApplyNodeTypeDefaults: """Tests for apply_node_type_defaults function.""" @@ -2778,49 +3128,56 @@ def test_no_node_attrs_is_noop(self): """When args lacks node type attrs, nothing happens.""" args = argparse.Namespace(command="test-config") apply_node_type_defaults(args) - assert not hasattr(args, 'single_node') - assert not hasattr(args, 'multi_node') + assert not hasattr(args, "single_node") + assert not hasattr(args, "multi_node") # ============================================================================= # Test generate_full_sweep mixed mode # ============================================================================= + class TestGenerateFullSweepMixed: """Tests for generate_full_sweep with both single-node and multi-node configs.""" - def test_both_flags_generates_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_both): + def test_both_flags_generates_mixed( + self, sample_mixed_config, sample_runner_config, full_sweep_args_both + ): """Both flags True should produce both single-node and multinode entries.""" result = generate_full_sweep( - full_sweep_args_both, - sample_mixed_config, - sample_runner_config + full_sweep_args_both, sample_mixed_config, sample_runner_config ) has_single = any("tp" in entry and "prefill" not in entry for entry in result) has_multi = any("prefill" in entry for entry in result) assert has_single, "Expected single-node entries in mixed output" assert has_multi, "Expected multinode entries in mixed output" - def test_single_node_only_from_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_single_node): + def test_single_node_only_from_mixed( + self, sample_mixed_config, sample_runner_config, full_sweep_args_single_node + ): """--single-node should skip multinode entries from mixed config.""" result = generate_full_sweep( - full_sweep_args_single_node, - sample_mixed_config, - sample_runner_config + full_sweep_args_single_node, sample_mixed_config, sample_runner_config ) assert len(result) > 0 - assert all("prefill" not in entry for entry in result), "No multinode entries expected" - assert all("tp" in entry for entry in result), "All entries should have tp field" + assert all("prefill" not in entry for entry in result), ( + "No multinode entries expected" + ) + assert all("tp" in entry for entry in result), ( + "All entries should have tp field" + ) - def test_multi_node_only_from_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_multi_node): + def test_multi_node_only_from_mixed( + self, sample_mixed_config, sample_runner_config, full_sweep_args_multi_node + ): """--multi-node should skip single-node entries from mixed config.""" result = generate_full_sweep( - full_sweep_args_multi_node, - sample_mixed_config, - sample_runner_config + full_sweep_args_multi_node, sample_mixed_config, sample_runner_config ) assert len(result) > 0 - assert all("prefill" in entry for entry in result), "All entries should be multinode" + assert all("prefill" in entry for entry in result), ( + "All entries should be multinode" + ) def test_node_type_filters_apply_to_agentic_configs( self, @@ -2839,11 +3196,18 @@ def test_node_type_filters_apply_to_agentic_configs( "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [{ - "search-space": [ - {"tp": 4, "pp": 2, "kv-offloading": "none", "conc-list": [16]}, - ], - }], + "agentic-coding": [ + { + "search-space": [ + { + "tp": 4, + "pp": 2, + "kv-offloading": "none", + "conc-list": [16], + }, + ], + } + ], }, }, "dsv4-agentic-multinode": { @@ -2857,15 +3221,35 @@ def test_node_type_filters_apply_to_agentic_configs( "disagg": True, "kv-p2p-transfer": "nixl", "scenarios": { - "agentic-coding": [{ - "search-space": [ - { - "conc-list": [16], - "prefill": {"hardware": "gb200", "num-worker": 2, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 2, "ep": 4, "dp-attn": False}, - "decode": {"hardware": "h100", "num-worker": 1, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 1, "ep": 1, "dp-attn": False}, - }, - ], - }], + "agentic-coding": [ + { + "search-space": [ + { + "conc-list": [16], + "prefill": { + "hardware": "gb200", + "num-worker": 2, + "tp": 4, + "pp": 2, + "dcp-size": 2, + "pcp-size": 2, + "ep": 4, + "dp-attn": False, + }, + "decode": { + "hardware": "h100", + "num-worker": 1, + "tp": 4, + "pp": 2, + "dcp-size": 2, + "pcp-size": 1, + "ep": 1, + "dp-attn": False, + }, + }, + ], + } + ], }, }, } @@ -2922,17 +3306,14 @@ def test_qwen_b300_fp4_fp8_memory_tier_matrix_is_balanced(self): assert len(result) == 8 assert { - (row["precision"], row["kv-offloading"], row["conc"]) - for row in result + (row["precision"], row["kv-offloading"], row["conc"]) for row in result } == { (precision, offload, conc) for precision in ("fp4", "fp8") for offload in ("none", "dram") for conc in (16, 32) } - assert {row["image"] for row in result} == { - "lmsysorg/sglang:v0.5.16-cu130" - } + assert {row["image"] for row in result} == {"lmsysorg/sglang:v0.5.16-cu130"} assert all(row["runner"] == "cluster:b300-nv" for row in result) assert all(row["tp"] == 2 and row["ep"] == 2 for row in result) assert all(row["spec-decoding"] == "mtp" for row in result) @@ -2949,10 +3330,51 @@ def test_qwen_b300_fp4_fp8_memory_tier_matrix_is_balanced(self): ) +# ============================================================================= +# Test filter_exp_names +# ============================================================================= + + +class TestFilterExpNames: + def test_selects_exact_names_in_matrix_order(self): + entries = [ + {"exp-name": "deployment-a", "conc": 1}, + {"exp-name": "deployment-b", "conc": 1}, + {"exp-name": "deployment-c", "conc": 2}, + ] + + result = filter_exp_names(entries, ["deployment-b", "deployment-a"]) + + assert result == entries[:2] + + @pytest.mark.parametrize( + ("entries", "names", "message"), + ( + ([{"exp-name": "deployment-a"}], ["missing"], "not found"), + ( + [{"exp-name": "deployment-a"}, {"exp-name": "deployment-a"}], + ["deployment-a"], + "multiple rows", + ), + ( + [{"exp-name": "deployment-a"}], + ["deployment-a", "deployment-a"], + "duplicate values", + ), + ), + ) + def test_rejects_missing_ambiguous_or_duplicate_names( + self, entries, names, message + ): + with pytest.raises(ValueError, match=message): + filter_exp_names(entries, names) + + # ============================================================================= # Test expand_config_keys # ============================================================================= + class TestExpandConfigKeys: """Tests for expand_config_keys glob/wildcard matching.""" @@ -3006,9 +3428,7 @@ def test_missing_exact_key_raises(self): def test_mixed_exact_and_glob(self): """Mix of exact keys and glob patterns should work.""" - result = expand_config_keys( - ["dsr1-fp8-h200-trt", "gptoss*"], self.AVAILABLE - ) + result = expand_config_keys(["dsr1-fp8-h200-trt", "gptoss*"], self.AVAILABLE) assert result == [ "dsr1-fp8-h200-trt", "gptoss-fp4-b200-vllm", @@ -3030,14 +3450,15 @@ def test_overlapping_patterns_deduplicate(self): # Tests for e2e-tests.yml workflow config splitting # ============================================================================= + def _split_e2e_configs(data): """Replicate the splitting logic from e2e-tests.yml get-jobs step. Returns (SINGLE, MULTI, EVALS) lists matching the workflow filters. """ - single = [x for x in data if 'prefill' not in x and not x.get('eval-only', False)] - multi = [x for x in data if 'prefill' in x and not x.get('eval-only', False)] - evals = [x for x in data if 'prefill' not in x and x.get('run-eval', False)] + single = [x for x in data if "prefill" not in x and not x.get("eval-only", False)] + multi = [x for x in data if "prefill" in x and not x.get("eval-only", False)] + evals = [x for x in data if "prefill" not in x and x.get("run-eval", False)] return single, multi, evals @@ -3051,38 +3472,66 @@ def mixed_entries(self): """Simulates default mode output: single-node (some eval-marked), plus multi-node entries.""" return [ - {'exp-name': 'a', 'isl': 1024, 'osl': 1024, 'conc': 64, 'tp': 2, 'run-eval': False}, - {'exp-name': 'b', 'isl': 1024, 'osl': 1024, 'conc': 128, 'tp': 2, 'run-eval': False}, - {'exp-name': 'c', 'isl': 8192, 'osl': 1024, 'conc': 256, 'tp': 2, 'run-eval': True}, - {'exp-name': 'd', 'isl': 8192, 'osl': 1024, 'conc': 512, 'tp': 2, 'run-eval': True}, - {'exp-name': 'e', 'conc': 64, 'prefill': {'tp': 2, 'num-worker': 1}}, + { + "exp-name": "a", + "isl": 1024, + "osl": 1024, + "conc": 64, + "tp": 2, + "run-eval": False, + }, + { + "exp-name": "b", + "isl": 1024, + "osl": 1024, + "conc": 128, + "tp": 2, + "run-eval": False, + }, + { + "exp-name": "c", + "isl": 8192, + "osl": 1024, + "conc": 256, + "tp": 2, + "run-eval": True, + }, + { + "exp-name": "d", + "isl": 8192, + "osl": 1024, + "conc": 512, + "tp": 2, + "run-eval": True, + }, + {"exp-name": "e", "conc": 64, "prefill": {"tp": 2, "num-worker": 1}}, ] def test_default_mode_benchmarks_all_single_node(self, mixed_entries): """Default: all single-node entries (including eval-marked) are benchmarked.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(single) == 4 - assert all('prefill' not in x for x in single) + assert all("prefill" not in x for x in single) def test_default_mode_evals_only_eval_marked(self, mixed_entries): """Default: only eval-marked entries go to EVALS.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(evals) == 2 - assert all(x['run-eval'] for x in evals) + assert all(x["run-eval"] for x in evals) def test_default_mode_eval_marked_in_both(self, mixed_entries): """Default: eval-marked entries appear in BOTH single and evals.""" single, multi, evals = _split_e2e_configs(mixed_entries) - eval_names = {x['exp-name'] for x in evals} - single_names = {x['exp-name'] for x in single} + eval_names = {x["exp-name"] for x in evals} + single_names = {x["exp-name"] for x in single} assert eval_names.issubset(single_names) def test_no_evals_all_benchmarked(self): """--no-evals: mark_eval_entries is skipped, no run-eval=True entries.""" data = [ - {'exp-name': 'a', 'conc': 64, 'tp': 2, 'run-eval': False}, - {'exp-name': 'b', 'conc': 128, 'tp': 2, 'run-eval': False}, - {'exp-name': 'c', 'conc': 256, 'tp': 2, 'run-eval': False}, + {"exp-name": "a", "conc": 64, "tp": 2, "run-eval": False}, + {"exp-name": "b", "conc": 128, "tp": 2, "run-eval": False}, + {"exp-name": "c", "conc": 256, "tp": 2, "run-eval": False}, ] single, multi, evals = _split_e2e_configs(data) assert len(single) == 3 @@ -3091,8 +3540,20 @@ def test_no_evals_all_benchmarked(self): def test_evals_only_no_benchmarks(self): """--evals-only: entries have eval-only flag, SINGLE must be empty.""" data = [ - {'exp-name': 'c', 'conc': 256, 'tp': 2, 'run-eval': True, 'eval-only': True}, - {'exp-name': 'd', 'conc': 512, 'tp': 2, 'run-eval': True, 'eval-only': True}, + { + "exp-name": "c", + "conc": 256, + "tp": 2, + "run-eval": True, + "eval-only": True, + }, + { + "exp-name": "d", + "conc": 512, + "tp": 2, + "run-eval": True, + "eval-only": True, + }, ] single, multi, evals = _split_e2e_configs(data) assert len(single) == 0, "evals-only should not trigger benchmarks" @@ -3100,10 +3561,22 @@ def test_evals_only_no_benchmarks(self): def test_all_evals_routes_every_fixed_sequence_entry_to_evals(self): data = [ - {'exp-name': 'a', 'isl': 1024, 'conc': 4, 'tp': 2, - 'run-eval': True, 'eval-only': True}, - {'exp-name': 'b', 'isl': 8192, 'conc': 8, 'tp': 2, - 'run-eval': True, 'eval-only': True}, + { + "exp-name": "a", + "isl": 1024, + "conc": 4, + "tp": 2, + "run-eval": True, + "eval-only": True, + }, + { + "exp-name": "b", + "isl": 8192, + "conc": 8, + "tp": 2, + "run-eval": True, + "eval-only": True, + }, ] single, multi, evals = _split_e2e_configs(data) @@ -3122,17 +3595,19 @@ def test_all_eval_marked_without_eval_only_flag_still_benchmarked(self): 8k1k with single conc). Without eval-only flag, SINGLE must still include them for benchmarking.""" data = [ - {'exp-name': 'a', 'conc': 64, 'tp': 2, 'run-eval': True}, - {'exp-name': 'b', 'conc': 64, 'tp': 4, 'run-eval': True}, + {"exp-name": "a", "conc": 64, "tp": 2, "run-eval": True}, + {"exp-name": "b", "conc": 64, "tp": 4, "run-eval": True}, ] single, multi, evals = _split_e2e_configs(data) - assert len(single) == 2, "all-eval-marked entries must still be benchmarked in default mode" + assert len(single) == 2, ( + "all-eval-marked entries must still be benchmarked in default mode" + ) assert len(evals) == 2 def test_prefill_entries_never_in_single_or_evals(self, mixed_entries): """Prefill (multi-node) entries only appear in MULTI.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(multi) == 1 - assert all('prefill' in x for x in multi) - assert all('prefill' not in x for x in single) - assert all('prefill' not in x for x in evals) + assert all("prefill" in x for x in multi) + assert all("prefill" not in x for x in single) + assert all("prefill" not in x for x in evals) From 07bfc0dc60aea40865388223369abe5d41aaf425 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:25:49 -0500 Subject: [PATCH 65/99] chore: remove unintended generator formatting churn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:移除非预期的生成器格式化改动 --- utils/matrix_logic/generate_sweep_configs.py | 436 ++-- .../test_generate_sweep_configs.py | 1782 +++++++---------- 2 files changed, 860 insertions(+), 1358 deletions(-) diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 3f8aa13874..622d06527f 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -21,7 +21,10 @@ validate_matrix_entry, ) -seq_len_stoi = {"1k1k": (1024, 1024), "8k1k": (8192, 1024)} +seq_len_stoi = { + "1k1k": (1024, 1024), + "8k1k": (8192, 1024) +} MIN_EVAL_CONC = 16 # Bound how many multinode agentic conc points share one server allocation. @@ -44,7 +47,6 @@ def seq_len_to_str(isl: int, osl: int) -> str: """ return seq_len_itos.get((isl, osl), f"{isl}_{osl}") - def freeze_config_value(value): """Convert JSON-shaped config values into deterministic hashable values.""" if isinstance(value, dict): @@ -107,7 +109,9 @@ def minimum_concurrency(entry: dict): kept_entry = {**kept_entry, "run-eval": True} if kept_entry.get("prefill") is not None: kept_entry["eval-conc"] = minimum_concurrency(kept_entry) - if any(out[index].get("eval-all-concs") is True for index in indices): + if any( + out[index].get("eval-all-concs") is True for index in indices + ): kept_entry["eval-all-concs"] = True out[keep] = kept_entry drop.update(index for index in indices if index != keep) @@ -179,7 +183,9 @@ def scheduling_gpus_per_node(label: str, runner_data: dict) -> int: if len(matches) == 1: return matches.pop() if not matches: - raise ValueError(f"Cannot resolve {Fields.GPUS_PER_NODE.value} for '{label}'") + raise ValueError( + f"Cannot resolve {Fields.GPUS_PER_NODE.value} for '{label}'" + ) raise ValueError( f"Ambiguous {Fields.GPUS_PER_NODE.value} for '{label}': {sorted(matches)}" ) @@ -265,9 +271,10 @@ def multinode_node_count( recipe_count = recipe_node_count(prefill, decode) if recipe_count is not None: return recipe_count - return worker_node_count( - prefill, "prefill", runner, runner_data - ) + worker_node_count(decode, "decode", runner, runner_data) + return ( + worker_node_count(prefill, "prefill", runner, runner_data) + + worker_node_count(decode, "decode", runner, runner_data) + ) def add_multinode_node_count( @@ -300,7 +307,6 @@ def effective_gpu_count(benchmark: dict) -> int: * benchmark.get(Fields.PCP_SIZE.value, 1) ) - def with_worker_parallelism_defaults(worker: dict) -> dict: """Return a worker config with explicit parallelism defaults.""" return { @@ -326,8 +332,7 @@ def multinode_worker_pair(benchmark: dict, disagg: bool) -> tuple[dict, dict]: **{ key: value for key, value in worker.items() - if key - not in ( + if key not in ( Fields.NUM_WORKER.value, Fields.ADDITIONAL_SETTINGS.value, ) @@ -398,9 +403,11 @@ def agentic_dram_offload_gb( gpus_per_node = runner_gpus_per_node(runner, runner_data) if Fields.WORKER.value in benchmark: - gpu_count = worker_gpus_per_node(benchmark[Fields.WORKER.value], gpus_per_node) + gpu_count = worker_gpus_per_node( + benchmark[Fields.WORKER.value], gpus_per_node) elif Fields.PREFILL.value in benchmark: - gpu_count = worker_gpus_per_node(benchmark[Fields.PREFILL.value], gpus_per_node) + gpu_count = worker_gpus_per_node( + benchmark[Fields.PREFILL.value], gpus_per_node) else: gpu_count = effective_gpu_count(benchmark) if gpu_count > gpus_per_node: @@ -412,7 +419,8 @@ def agentic_dram_offload_gb( f"{Fields.GPUS_PER_NODE.value}={gpus_per_node} for runner '{runner}'" ) proportional_bytes = ( - Decimal(available_mib) * BYTES_PER_MIB * utilization * gpu_count / gpus_per_node + Decimal(available_mib) * BYTES_PER_MIB * utilization + * gpu_count / gpus_per_node ) return int(proportional_bytes / BYTES_PER_GB) @@ -440,7 +448,8 @@ def _worker_tag(worker: dict, role_prefix: str) -> str: ep = worker.get(Fields.EP.value, 1) dpa = worker.get(Fields.DP_ATTN.value, False) tag = ( - f"{role_prefix}{worker[Fields.NUM_WORKER.value]}x{worker[Fields.TP.value]}" + f"{role_prefix}{worker[Fields.NUM_WORKER.value]}" + f"x{worker[Fields.TP.value]}" ) if ep != 1: tag += f"ep{ep}" @@ -468,17 +477,16 @@ def component_metadata(benchmark: dict, config: dict) -> dict: def chunk_multinode_agentic_concurrencies(conc_values: list[int]) -> list[list[int]]: """Bound sequential agentic profiles sharing one server allocation.""" size = MAX_MULTINODE_AGENTIC_CONCURRENCIES_PER_ALLOCATION - return [ - conc_values[index : index + size] for index in range(0, len(conc_values), size) - ] + return [conc_values[index:index + size] for index in range(0, len(conc_values), size)] def _freeze_matrix_value(value): """Convert nested matrix values into hashable equivalents.""" if isinstance(value, dict): - return tuple( - sorted((key, _freeze_matrix_value(item)) for key, item in value.items()) - ) + return tuple(sorted( + (key, _freeze_matrix_value(item)) + for key, item in value.items() + )) if isinstance(value, list): return tuple(_freeze_matrix_value(item) for item in value) return value @@ -502,18 +510,14 @@ def _multinode_parallelism_key(entry: dict) -> tuple: Fields.EVAL_ALL_CONCS.value, Fields.EXP_NAME.value, } - return tuple( - sorted( - (key, _freeze_matrix_value(value)) - for key, value in entry.items() - if key not in ignored_fields - ) - ) + return tuple(sorted( + (key, _freeze_matrix_value(value)) + for key, value in entry.items() + if key not in ignored_fields + )) -def mark_eval_entries( - matrix_values: list[dict], include_agentic: bool = False -) -> list[dict]: +def mark_eval_entries(matrix_values: list[dict], include_agentic: bool = False) -> list[dict]: """Eval selection policy: - Single-node: only consider 8k1k (isl=8192, osl=1024). For each unique (model, runner, framework, precision, isl, osl, spec-decoding, dp-attn): @@ -550,10 +554,7 @@ def _eligible_eval_concs(entry): for i, entry in enumerate(matrix_values): if Fields.TP.value not in entry: continue - if ( - entry.get(Fields.ISL.value) != target_isl - or entry.get(Fields.OSL.value) != target_osl - ): + if entry.get(Fields.ISL.value) != target_isl or entry.get(Fields.OSL.value) != target_osl: continue if not _eligible_eval_concs(entry): continue @@ -585,10 +586,7 @@ def _eligible_eval_concs(entry): continue if Fields.PREFILL.value not in entry: continue - if ( - entry.get(Fields.ISL.value) != target_isl - or entry.get(Fields.OSL.value) != target_osl - ): + if entry.get(Fields.ISL.value) != target_isl or entry.get(Fields.OSL.value) != target_osl: continue eval_concs = _eligible_eval_concs(entry) if not eval_concs: @@ -608,15 +606,13 @@ def _eligible_eval_concs(entry): # The selected eval subset uses exactly one conc per group. ag_mn_groups = defaultdict(list) for i, entry in enumerate(matrix_values): - if entry.get(Fields.SCENARIO_TYPE.value) != "agentic-coding": + if entry.get(Fields.SCENARIO_TYPE.value) != 'agentic-coding': continue if Fields.PREFILL.value in entry: eval_concs = _eligible_eval_concs(entry) if not eval_concs: continue - ag_mn_groups[_multinode_parallelism_key(entry)].append( - (i, eval_concs[-1]) - ) + ag_mn_groups[_multinode_parallelism_key(entry)].append((i, eval_concs[-1])) continue conc = entry[Fields.CONC.value] conc_val = max(conc) if isinstance(conc, list) else conc @@ -663,7 +659,7 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: target_isl, target_osl = seq_len_stoi["8k1k"] for entry in matrix_values: - if entry.get(Fields.SCENARIO_TYPE.value) == "agentic-coding": + if entry.get(Fields.SCENARIO_TYPE.value) == 'agentic-coding': if Fields.PREFILL.value not in entry: entry[Fields.RUN_EVAL.value] = True expanded_entries.append(entry) @@ -704,9 +700,9 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: parallelism_key = _multinode_parallelism_key(entry) if parallelism_key in multinode_indices: existing = expanded_entries[multinode_indices[parallelism_key]] - existing[Fields.CONC.value] = sorted( - set(existing[Fields.CONC.value] + conc_values) - ) + existing[Fields.CONC.value] = sorted(set( + existing[Fields.CONC.value] + conc_values + )) continue batched_entry = { @@ -752,8 +748,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if invalid_runners: raise ValueError( f"Invalid runner type(s): {invalid_runners}. " - f"Valid runner types are: {', '.join(sorted(valid_runner_types))}" - ) + f"Valid runner types are: {', '.join(sorted(valid_runner_types))}") matrix_values = [] @@ -762,7 +757,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.seq_lens: seq_lens_filter = {seq_len_stoi[sl] for sl in args.seq_lens} - # Iterate through all configurations and apply filters as specified (this is just "selecting" + # Iterate through all configurations and apply filters as specified (this is just "selecting" # configs from all of the master configs subject to some pattern matching) for key, val in all_config_data.items(): # Filter by model prefix if specified @@ -788,14 +783,8 @@ def generate_full_sweep(args, all_config_data, runner_data): disagg = val.get(Fields.DISAGG.value, False) scenarios = val[Fields.SCENARIOS.value] - scenario_filter = ( - set(args.scenario_type) if getattr(args, "scenario_type", None) else None - ) - seq_len_configs = ( - scenarios.get(Fields.FIXED_SEQ_LEN.value, []) - if (scenario_filter is None or "fixed-seq-len" in scenario_filter) - else [] - ) + scenario_filter = set(args.scenario_type) if getattr(args, 'scenario_type', None) else None + seq_len_configs = scenarios.get(Fields.FIXED_SEQ_LEN.value, []) if (scenario_filter is None or 'fixed-seq-len' in scenario_filter) else [] image = val[Fields.IMAGE.value] model = val[Fields.MODEL.value] precision = val[Fields.PRECISION.value] @@ -808,8 +797,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.runner_node_filter: runner_nodes = runner_nodes_for_label(runner, runner_data) runner_nodes_to_use = [ - node for node in runner_nodes if args.runner_node_filter in node - ] + node for node in runner_nodes if args.runner_node_filter in node] if not runner_nodes_to_use: # No matching nodes for this config's runner type, skip this config continue @@ -879,9 +867,7 @@ def generate_full_sweep(args, all_config_data, runner_data): conc_values = filtered_conc seq_len_str = seq_len_to_str(isl, osl) - runners_for_entry = ( - runner_nodes_to_use if runner_nodes_to_use else [runner] - ) + runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] for runner_value in runners_for_entry: entry = { @@ -944,7 +930,8 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.min_conc <= 0: continue conc_values = [ - conc for conc in conc_values if conc >= args.min_conc + conc for conc in conc_values + if conc >= args.min_conc ] if not conc_values: continue @@ -953,10 +940,13 @@ def generate_full_sweep(args, all_config_data, runner_data): if args.max_conc <= 0: continue filtered_conc = [ - conc for conc in conc_values if conc <= args.max_conc + conc for conc in conc_values + if conc <= args.max_conc ] conc_values = ( - filtered_conc if filtered_conc else [args.max_conc] + filtered_conc + if filtered_conc + else [args.max_conc] ) else: conc_start = bmk[Fields.CONC_START.value] @@ -991,9 +981,7 @@ def generate_full_sweep(args, all_config_data, runner_data): conc = conc_end seq_len_str = seq_len_to_str(isl, osl) - runners_for_entry = ( - runner_nodes_to_use if runner_nodes_to_use else [runner] - ) + runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] for conc in conc_values: for runner_value in runners_for_entry: @@ -1030,11 +1018,7 @@ def generate_full_sweep(args, all_config_data, runner_data): matrix_values.append(entry) # ---- Agentic-coding scenarios ---- - agentic_configs = ( - scenarios.get(Fields.AGENTIC_CODING.value, []) - if (scenario_filter is None or "agentic-coding" in scenario_filter) - else [] - ) + agentic_configs = scenarios.get(Fields.AGENTIC_CODING.value, []) if (scenario_filter is None or 'agentic-coding' in scenario_filter) else [] if is_multinode and not args.multi_node: continue if not is_multinode and not args.single_node: @@ -1061,8 +1045,7 @@ def generate_full_sweep(args, all_config_data, runner_data): kv_offloading = bmk[Fields.KV_OFFLOADING.value] kv_offload_backend = bmk.get(Fields.KV_OFFLOAD_BACKEND.value) total_cpu_dram_gb = agentic_dram_offload_gb( - agentic_config, bmk, runner, runner_data - ) + agentic_config, bmk, runner, runner_data) # Get concurrency values conc_list = bmk.get(Fields.CONC_LIST.value) @@ -1089,9 +1072,7 @@ def generate_full_sweep(args, all_config_data, runner_data): if not conc_values: continue - runners_for_entry = ( - runner_nodes_to_use if runner_nodes_to_use else [runner] - ) + runners_for_entry = runner_nodes_to_use if runner_nodes_to_use else [runner] if is_multinode: # Preserve historical exp-names for the default (no offload) @@ -1102,9 +1083,7 @@ def generate_full_sweep(args, all_config_data, runner_data): else "" ) for runner_value in runners_for_entry: - for conc_batch in chunk_multinode_agentic_concurrencies( - conc_values - ): + for conc_batch in chunk_multinode_agentic_concurrencies(conc_values): entry = { Fields.IMAGE.value: image, Fields.MODEL.value: model, @@ -1120,19 +1099,13 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb, Fields.DURATION.value: duration, Fields.EXP_NAME.value: multinode_agentic_exp_name( - model_code, - prefill, - decode, - conc_batch, - offload_suffix, + model_code, prefill, decode, conc_batch, offload_suffix ), Fields.DISAGG.value: disagg, Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = ( - kv_offload_backend - ) + entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend entry.update(component_metadata(bmk, val)) add_multinode_node_count( entry, @@ -1156,9 +1129,7 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.DCP_SIZE.value: dcp_size, Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn - if dp_attn is not None - else False, + Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.CONC.value: conc, Fields.KV_OFFLOADING.value: kv_offloading, @@ -1167,18 +1138,12 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.EXP_NAME.value: ( f"{model_code}_tp{tp}_conc{conc}_" f"{agentic_kv_offload_suffix(kv_offloading, kv_offload_backend)}" - + ( - f"_spec-{spec_decoding}" - if spec_decoding != "none" - else "" - ) + + (f"_spec-{spec_decoding}" if spec_decoding != "none" else "") ), Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = ( - kv_offload_backend - ) + entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend entry.update(component_metadata(bmk, val)) validate_agentic_matrix_entry(entry) matrix_values.append(entry) @@ -1186,9 +1151,7 @@ def generate_full_sweep(args, all_config_data, runner_data): return matrix_values -def _runner_values_for_filter( - runner: str, runner_data: dict, runner_node_filter: str | None -) -> list[str]: +def _runner_values_for_filter(runner: str, runner_data: dict, runner_node_filter: str | None) -> list[str]: if not runner_node_filter: return [runner] @@ -1228,25 +1191,18 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): framework = val[Fields.FRAMEWORK.value] runner = val[Fields.RUNNER.value] runners_for_entry = _runner_values_for_filter( - runner, runner_data, getattr(args, "runner_node_filter", None) - ) + runner, runner_data, getattr(args, 'runner_node_filter', None)) if not runners_for_entry: continue disagg = val.get(Fields.DISAGG.value, False) # Build seq-len filter if --seq-lens was provided seq_lens_filter = None - if getattr(args, "seq_lens", None): + if getattr(args, 'seq_lens', None): seq_lens_filter = {seq_len_stoi[s] for s in args.seq_lens} - scenario_filter = ( - set(args.scenario_type) if getattr(args, "scenario_type", None) else None - ) - fixed_configs = ( - val[Fields.SCENARIOS.value].get(Fields.FIXED_SEQ_LEN.value, []) - if (scenario_filter is None or "fixed-seq-len" in scenario_filter) - else [] - ) + scenario_filter = set(args.scenario_type) if getattr(args, 'scenario_type', None) else None + fixed_configs = val[Fields.SCENARIOS.value].get(Fields.FIXED_SEQ_LEN.value, []) if (scenario_filter is None or 'fixed-seq-len' in scenario_filter) else [] for seq_len_config in fixed_configs: isl = seq_len_config[Fields.ISL.value] osl = seq_len_config[Fields.OSL.value] @@ -1279,7 +1235,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): conc = conc_end # Apply --conc filter if provided (only for test-config) - if getattr(args, "conc", None): + if getattr(args, 'conc', None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: # No intersection with requested conc values; skip @@ -1310,9 +1266,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): runner_data, bmk.get(Fields.NUM_NODES.value), ) - matrix_values.append( - validate_matrix_entry(entry, is_multinode=True) - ) + matrix_values.append(validate_matrix_entry(entry, is_multinode=True)) else: # Single-node config tp = bmk[Fields.TP.value] @@ -1340,7 +1294,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): conc = conc_end # Apply --conc filter if provided (only for test-config) - if getattr(args, "conc", None): + if getattr(args, 'conc', None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: # No intersection with requested conc values; skip @@ -1364,25 +1318,17 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.CONC.value: conc, Fields.MAX_MODEL_LEN.value: isl + osl + 256, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn - if dp_attn is not None - else False, + Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.EXP_NAME.value: f"{model_code}_{seq_len_str}", Fields.DISAGG.value: disagg, Fields.RUN_EVAL.value: False, } entry.update(component_metadata(bmk, val)) - matrix_values.append( - validate_matrix_entry(entry, is_multinode=False) - ) + matrix_values.append(validate_matrix_entry(entry, is_multinode=False)) # ---- Agentic-coding scenarios ---- - agentic_configs = ( - val[Fields.SCENARIOS.value].get(Fields.AGENTIC_CODING.value, []) - if (scenario_filter is None or "agentic-coding" in scenario_filter) - else [] - ) + agentic_configs = val[Fields.SCENARIOS.value].get(Fields.AGENTIC_CODING.value, []) if (scenario_filter is None or 'agentic-coding' in scenario_filter) else [] for agentic_config in agentic_configs: duration = DEFAULT_AGENTIC_DURATION_SECONDS bmk_space = agentic_config[Fields.SEARCH_SPACE.value] @@ -1404,8 +1350,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): kv_offloading = bmk[Fields.KV_OFFLOADING.value] kv_offload_backend = bmk.get(Fields.KV_OFFLOAD_BACKEND.value) total_cpu_dram_gb = agentic_dram_offload_gb( - agentic_config, bmk, runner, runner_data - ) + agentic_config, bmk, runner, runner_data) conc_list = bmk.get(Fields.CONC_LIST.value) if conc_list: @@ -1423,7 +1368,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): if conc > conc_end: conc = conc_end - if getattr(args, "conc", None): + if getattr(args, 'conc', None): conc_values = [c for c in conc_values if c in args.conc] if not conc_values: continue @@ -1437,9 +1382,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): else "" ) for runner_value in runners_for_entry: - for conc_batch in chunk_multinode_agentic_concurrencies( - conc_values - ): + for conc_batch in chunk_multinode_agentic_concurrencies(conc_values): entry = { Fields.IMAGE.value: image, Fields.MODEL.value: model, @@ -1455,19 +1398,13 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb, Fields.DURATION.value: duration, Fields.EXP_NAME.value: multinode_agentic_exp_name( - model_code, - prefill, - decode, - conc_batch, - offload_suffix, + model_code, prefill, decode, conc_batch, offload_suffix ), Fields.DISAGG.value: disagg, Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = ( - kv_offload_backend - ) + entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend entry.update(component_metadata(bmk, val)) add_multinode_node_count( entry, @@ -1490,9 +1427,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.DCP_SIZE.value: dcp_size, Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, - Fields.DP_ATTN.value: dp_attn - if dp_attn is not None - else False, + Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, Fields.SPEC_DECODING.value: spec_decoding, Fields.CONC.value: conc, Fields.KV_OFFLOADING.value: kv_offloading, @@ -1501,18 +1436,12 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.EXP_NAME.value: ( f"{model_code}_tp{tp}_conc{conc}_" f"{agentic_kv_offload_suffix(kv_offloading, kv_offload_backend)}" - + ( - f"_spec-{spec_decoding}" - if spec_decoding != "none" - else "" - ) + + (f"_spec-{spec_decoding}" if spec_decoding != "none" else "") ), Fields.SCENARIO_TYPE.value: "agentic-coding", } if kv_offload_backend is not None: - entry[Fields.KV_OFFLOAD_BACKEND.value] = ( - kv_offload_backend - ) + entry[Fields.KV_OFFLOAD_BACKEND.value] = kv_offload_backend entry.update(component_metadata(bmk, val)) matrix_values.append(validate_agentic_matrix_entry(entry)) @@ -1531,7 +1460,7 @@ def expand_config_keys(config_keys, available_keys): available = list(available_keys) seen = {} # use dict to preserve insertion order for key in config_keys: - if "*" in key or "?" in key: + if '*' in key or '?' in key: matches = fnmatch.filter(available, key) if not matches: raise ValueError( @@ -1570,12 +1499,16 @@ def filter_exp_names(entries: list[dict], exp_names: list[str]) -> list[dict]: raise ValueError( "Experiment name(s) matched multiple rows: " + ", ".join(ambiguous) ) - return [entry for entry in entries if entry.get(Fields.EXP_NAME.value) in requested] + return [ + entry + for entry in entries + if entry.get(Fields.EXP_NAME.value) in requested + ] def apply_node_type_defaults(args): """Default both single_node and multi_node to True when neither is specified.""" - if hasattr(args, "single_node") and hasattr(args, "multi_node"): + if hasattr(args, 'single_node') and hasattr(args, 'multi_node'): if not args.single_node and not args.multi_node: args.single_node = True args.multi_node = True @@ -1586,194 +1519,200 @@ def main(): # Create parent parser with common arguments parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - "--config-files", - nargs="+", + '--config-files', + nargs='+', required=True, - help="One or more configuration files (YAML format)", + help='One or more configuration files (YAML format)' ) parent_parser.add_argument( - "--runner-config", - default="configs/runners.yaml", - help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", + '--runner-config', + default='configs/runners.yaml', + help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' ) eval_group = parent_parser.add_mutually_exclusive_group() eval_group.add_argument( - "--no-evals", - action="store_true", - help="When specified, skip evals (throughput benchmarks only).", + '--no-evals', + action='store_true', + help='When specified, skip evals (throughput benchmarks only).' ) eval_group.add_argument( - "--evals-only", - action="store_true", - help="When specified, run ONLY the eval subset (excludes non-eval configs).", + '--evals-only', + action='store_true', + help='When specified, run ONLY the eval subset (excludes non-eval configs).' ) parent_parser.add_argument( - "--all-evals", - action="store_true", + '--all-evals', + action='store_true', help=( - "Expand eval selection to every generated fixed-sequence config. " - "Can be combined with --evals-only; used alone, it also emits eval-only jobs." - ), + 'Expand eval selection to every generated fixed-sequence config. ' + 'Can be combined with --evals-only; used alone, it also emits eval-only jobs.' + ) ) parent_parser.add_argument( - "--trim-conc", - action="store_true", + '--trim-conc', + action='store_true', help=( - "Trim each generated deployment shape to its minimum concurrency " - "after applying eval selection." - ), + 'Trim each generated deployment shape to its minimum concurrency ' + 'after applying eval selection.' + ) ) parent_parser.add_argument( - "--runner-node-filter", + '--runner-node-filter', required=False, - help='Filter runner nodes by substring match (e.g., "amd" to only include nodes containing that string). Expands each config to individual matching nodes.', + help='Filter runner nodes by substring match (e.g., "amd" to only include nodes containing that string). Expands each config to individual matching nodes.' ) parent_parser.add_argument( - "--scenario-type", - nargs="+", - choices=["fixed-seq-len", "agentic-coding"], + '--scenario-type', + nargs='+', + choices=['fixed-seq-len', 'agentic-coding'], required=False, - help="Scenario type(s) to include. If not specified, all scenario types are generated.", + help='Scenario type(s) to include. If not specified, all scenario types are generated.' ) # Create main parser parser = argparse.ArgumentParser( - description="Generate benchmark configurations from YAML config files" + description='Generate benchmark configurations from YAML config files' ) # Create subparsers for subcommands subparsers = parser.add_subparsers( - dest="command", required=True, help="Available commands" + dest='command', + required=True, + help='Available commands' ) # Subcommand: full-sweep full_sweep_parser = subparsers.add_parser( - "full-sweep", + 'full-sweep', parents=[parent_parser], add_help=False, - help="Generate full sweep configurations with optional filtering by model, precision, framework, runner type, and sequence lengths", + help='Generate full sweep configurations with optional filtering by model, precision, framework, runner type, and sequence lengths' ) full_sweep_parser.add_argument( - "--model-prefix", - nargs="+", + '--model-prefix', + nargs='+', required=False, - help="Model prefix(es) to filter configurations (optional, can specify multiple)", + help='Model prefix(es) to filter configurations (optional, can specify multiple)' ) full_sweep_parser.add_argument( - "--precision", - nargs="+", + '--precision', + nargs='+', required=False, - help="Precision(s) to filter by (e.g., fp4, fp8) (optional, can specify multiple)", + help='Precision(s) to filter by (e.g., fp4, fp8) (optional, can specify multiple)' ) full_sweep_parser.add_argument( - "--framework", - nargs="+", + '--framework', + nargs='+', required=False, - help="Framework(s) to filter by (e.g., vllm, trt, sglang) (optional, can specify multiple)", + help='Framework(s) to filter by (e.g., vllm, trt, sglang) (optional, can specify multiple)' ) full_sweep_parser.add_argument( - "--runner-type", - nargs="+", + '--runner-type', + nargs='+', required=False, - help="Runner type(s) to filter by (e.g., h200, h100) (optional, can specify multiple)", + help='Runner type(s) to filter by (e.g., h200, h100) (optional, can specify multiple)' ) full_sweep_parser.add_argument( - "--seq-lens", - nargs="+", + '--seq-lens', + nargs='+', choices=list(seq_len_stoi.keys()), required=False, - help=f"Sequence length configurations to include: {', '.join(seq_len_stoi.keys())}. If not specified, all sequence lengths are included.", + help=f"Sequence length configurations to include: {', '.join(seq_len_stoi.keys())}. If not specified, all sequence lengths are included." ) full_sweep_parser.add_argument( - "--step-size", + '--step-size', type=int, default=2, - help="Step size for concurrency values (default: 2)", + help='Step size for concurrency values (default: 2)' ) full_sweep_parser.add_argument( - "--min-conc", + '--min-conc', type=int, required=False, - help="Minimum concurrency value to include (filters out lower concurrency values)", + help='Minimum concurrency value to include (filters out lower concurrency values)' ) full_sweep_parser.add_argument( - "--max-conc", + '--max-conc', type=int, required=False, - help="Maximum concurrency value to include (filters out higher concurrency values)", + help='Maximum concurrency value to include (filters out higher concurrency values)' ) full_sweep_parser.add_argument( - "--max-tp", + '--max-tp', type=int, required=False, - help="Maximum tensor parallelism value to include (single-node only)", + help='Maximum tensor parallelism value to include (single-node only)' ) full_sweep_parser.add_argument( - "--max-ep", + '--max-ep', type=int, required=False, - help="Maximum expert parallelism value to include (single-node only)", + help='Maximum expert parallelism value to include (single-node only)' ) full_sweep_parser.add_argument( - "--single-node", - action="store_true", - help="Only generate single-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.", + '--single-node', + action='store_true', + help='Only generate single-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.' ) full_sweep_parser.add_argument( - "--multi-node", - action="store_true", - help="Only generate multi-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.", + '--multi-node', + action='store_true', + help='Only generate multi-node configurations. If neither --single-node nor --multi-node is specified, both types are generated.' ) full_sweep_parser.add_argument( - "-h", "--help", action="help", help="Show this help message and exit" + '-h', '--help', + action='help', + help='Show this help message and exit' ) # Subcommand: test-config test_config_keys_parser = subparsers.add_parser( - "test-config", + 'test-config', parents=[parent_parser], add_help=False, - help="Generate full sweep for specific config keys. Validates that all specified keys exist before generating.", + help='Generate full sweep for specific config keys. Validates that all specified keys exist before generating.' ) test_config_keys_parser.add_argument( - "--config-keys", - nargs="+", + '--config-keys', + nargs='+', required=True, - help="One or more config keys to generate sweep for (e.g., dsr1-fp4-b200-sglang dsr1-fp8-h200-trt)", + help='One or more config keys to generate sweep for (e.g., dsr1-fp4-b200-sglang dsr1-fp8-h200-trt)' ) test_config_keys_parser.add_argument( - "--conc", - nargs="+", + '--conc', + nargs='+', type=int, required=False, - help="Only include these concurrency values. Values must exist in the config conc-range/list.", + help='Only include these concurrency values. Values must exist in the config conc-range/list.' ) test_config_keys_parser.add_argument( - "--exp-names", - nargs="+", + '--exp-names', + nargs='+', required=False, help=( - "Only include exact generated experiment names. Each name must " - "match exactly one row after config and concurrency filtering." - ), + 'Only include exact generated experiment names. Each name must ' + 'match exactly one row after config and concurrency filtering.' + ) ) test_config_keys_parser.add_argument( - "--seq-lens", - nargs="+", + '--seq-lens', + nargs='+', choices=list(seq_len_stoi.keys()), required=False, - help="Only include these sequence length configurations (e.g., 1k1k 8k1k)", + help='Only include these sequence length configurations (e.g., 1k1k 8k1k)' ) test_config_keys_parser.add_argument( - "-h", "--help", action="help", help="Show this help message and exit" + '-h', '--help', + action='help', + help='Show this help message and exit' ) args = parser.parse_args() apply_node_type_defaults(args) - if args.command == "full-sweep" and args.step_size <= 1: + if args.command == 'full-sweep' and args.step_size <= 1: parser.error("--step-size must be greater than 1") if ( - args.command == "full-sweep" + args.command == 'full-sweep' and args.min_conc is not None and args.max_conc is not None and args.min_conc > args.max_conc @@ -1787,24 +1726,23 @@ def main(): runner_data = load_runner_file(args.runner_config) # Route to appropriate function based on subcommand - if args.command == "full-sweep": + if args.command == 'full-sweep': matrix_values = generate_full_sweep(args, all_config_data, runner_data) - elif args.command == "test-config": + elif args.command == 'test-config': matrix_values = generate_test_config_sweep(args, all_config_data, runner_data) else: parser.error(f"Unknown command: {args.command}") - if args.command == "test-config" and args.exp_names: + if args.command == 'test-config' and args.exp_names: try: matrix_values = filter_exp_names(matrix_values, args.exp_names) except ValueError as error: parser.error(str(error)) + # Apply the existing eval policy first, then expand it when requested. if not args.no_evals: - matrix_values = mark_eval_entries( - matrix_values, include_agentic=args.evals_only or args.all_evals - ) + matrix_values = mark_eval_entries(matrix_values, include_agentic=args.evals_only or args.all_evals) if args.all_evals: matrix_values = mark_all_eval_entries(matrix_values) @@ -1812,9 +1750,7 @@ def main(): matrix_values = trim_conc(matrix_values) if args.evals_only or args.all_evals: - matrix_values = [ - e for e in matrix_values if e.get(Fields.RUN_EVAL.value, False) - ] + matrix_values = [e for e in matrix_values if e.get(Fields.RUN_EVAL.value, False)] for entry in matrix_values: entry[Fields.EVAL_ONLY.value] = True diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index 6c700a58d2..d30f554194 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -1,5 +1,4 @@ """Comprehensive tests for generate_sweep_configs.py""" - import argparse import copy import hashlib @@ -103,10 +102,9 @@ def test_multinode_node_count_uses_role_gpu_footprints(sample_runner_config): prefill = {"num-worker": 3, "tp": 2, "pp": 1, "pcp-size": 1} decode = {"num-worker": 2, "tp": 8, "pp": 1, "pcp-size": 1} - assert ( - multinode_node_count(prefill, decode, "cluster:b300-nv", sample_runner_config) - == 3 - ) + assert multinode_node_count( + prefill, decode, "cluster:b300-nv", sample_runner_config + ) == 3 def test_multinode_node_count_honors_explicit_role_node_settings(): @@ -130,7 +128,9 @@ def test_multinode_node_count_resolves_heterogeneous_worker_hardware( prefill = {"hardware": "gb200", "num-worker": 5, "tp": 4} decode = {"hardware": "h100", "num-worker": 1, "tp": 8} - assert multinode_node_count(prefill, decode, "gb200", sample_runner_config) == 6 + assert multinode_node_count( + prefill, decode, "gb200", sample_runner_config + ) == 6 def test_multinode_node_count_prefers_checked_in_recipe_resources( @@ -148,10 +148,9 @@ def test_multinode_node_count_prefers_checked_in_recipe_resources( } decode = {"num-worker": 1, "tp": 1} - assert ( - multinode_node_count(prefill, decode, "cluster:gb200-nv", sample_runner_config) - == 7 - ) + assert multinode_node_count( + prefill, decode, "cluster:gb200-nv", sample_runner_config + ) == 7 def test_multinode_node_count_resolves_repo_relative_recipe_path( @@ -170,17 +169,15 @@ def test_multinode_node_count_resolves_repo_relative_recipe_path( } decode = {"num-worker": 0, "tp": 8} - assert ( - multinode_node_count(prefill, decode, "cluster:gb300-nv", sample_runner_config) - == 2 - ) + assert multinode_node_count( + prefill, decode, "cluster:gb300-nv", sample_runner_config + ) == 2 # ============================================================================= # Test Fixtures # ============================================================================= - @pytest.fixture def sample_single_node_config(): """Single node config based on dsr1-fp8-mi300x-sglang.""" @@ -195,18 +192,23 @@ def sample_single_node_config(): "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, - "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 64}], + "search-space": [ + {"tp": 8, "conc-start": 4, "conc-end": 64} + ] }, { "isl": 8192, "osl": 1024, - "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 64}], - }, + "search-space": [ + {"tp": 8, "conc-start": 4, "conc-end": 64} + ] + } ] - }, + } } } @@ -227,6 +229,7 @@ def sample_multinode_config(): "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, @@ -256,10 +259,10 @@ def sample_multinode_config(): ], }, } - ], + ] } ] - }, + } } } @@ -278,27 +281,12 @@ def sample_runner_config(): "gb200": ["gb200-nv_0"], }, "hardware": { - "cluster:h100-dgxc": { - "available-cpu-dram-mib": 2063837, - "gpus-per-node": 8, - }, - "cluster:h200-dgxc": { - "available-cpu-dram-mib": 1471356, - "gpus-per-node": 8, - }, - "cluster:b200-nscale": { - "available-cpu-dram-mib": 3774874, - "gpus-per-node": 8, - }, + "cluster:h100-dgxc": {"available-cpu-dram-mib": 2063837, "gpus-per-node": 8}, + "cluster:h200-dgxc": {"available-cpu-dram-mib": 1471356, "gpus-per-node": 8}, + "cluster:b200-nscale": {"available-cpu-dram-mib": 3774874, "gpus-per-node": 8}, "cluster:b300-nv": {"available-cpu-dram-mib": 2964436, "gpus-per-node": 8}, - "cluster:mi300x-amds": { - "available-cpu-dram-mib": 2321924, - "gpus-per-node": 8, - }, - "cluster:mi355x-amds": { - "available-cpu-dram-mib": 3095781, - "gpus-per-node": 8, - }, + "cluster:mi300x-amds": {"available-cpu-dram-mib": 2321924, "gpus-per-node": 8}, + "cluster:mi355x-amds": {"available-cpu-dram-mib": 3095781, "gpus-per-node": 8}, "cluster:gb200-nv": {"available-cpu-dram-mib": 860160, "gpus-per-node": 4}, }, } @@ -348,7 +336,6 @@ def full_sweep_args_multi_node(): # Test seq_len mappings # ============================================================================= - class TestSeqLenMappings: """Tests for sequence length string mappings.""" @@ -381,7 +368,6 @@ def test_unknown_sequence_lengths(self): # Test mark_eval_entries # ============================================================================= - class TestMarkEvalEntries: """Tests for eval matrix selection policy.""" @@ -389,21 +375,13 @@ def test_marks_agentic_entry_for_gsm8k(self): matrix_values = [ { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "vllm", - "precision": "fp4", - "tp": 8, - "conc": 32, + "model": "m", "runner": "b300", "framework": "vllm", + "precision": "fp4", "tp": 8, "conc": 32, }, { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "vllm", - "precision": "fp4", - "tp": 8, - "conc": 64, + "model": "m", "runner": "b300", "framework": "vllm", + "precision": "fp4", "tp": 8, "conc": 64, }, ] @@ -425,12 +403,8 @@ def test_marks_multinode_agentic_entry_at_highest_eligible_conc(self): """ common = { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "sglang-disagg", - "precision": "fp4", - "spec-decoding": "none", - "disagg": True, + "model": "m", "runner": "b300", "framework": "sglang-disagg", + "precision": "fp4", "spec-decoding": "none", "disagg": True, "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, } @@ -452,12 +426,8 @@ def test_multinode_agentic_groups_are_independent_per_topology(self): prefill EP/DP) must each get their own eval row.""" base = { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "sglang-disagg", - "precision": "fp4", - "spec-decoding": "none", - "disagg": True, + "model": "m", "runner": "b300", "framework": "sglang-disagg", + "precision": "fp4", "spec-decoding": "none", "disagg": True, } topology_a = { "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, @@ -485,21 +455,13 @@ def test_default_mode_does_not_mark_agentic(self): matrix_values = [ { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "vllm", - "precision": "fp4", - "tp": 8, - "conc": 32, + "model": "m", "runner": "b300", "framework": "vllm", + "precision": "fp4", "tp": 8, "conc": 32, }, { "scenario-type": "agentic-coding", - "model": "m", - "runner": "b300", - "framework": "vllm", - "precision": "fp4", - "tp": 8, - "conc": 64, + "model": "m", "runner": "b300", "framework": "vllm", + "precision": "fp4", "tp": 8, "conc": 64, }, ] @@ -660,7 +622,6 @@ def test_multi_node_marks_each_parallelism_at_highest_eligible_conc(self): def test_multi_node_worker_counts_define_parallelism(self): """Prefill and decode worker counts should each define a distinct eval target.""" - def entry(prefill_workers, decode_workers, conc): return { "model": "deepseek-ai/DeepSeek-R1-0528", @@ -685,13 +646,11 @@ def entry(prefill_workers, decode_workers, conc): "conc": [16, conc], } - result = mark_eval_entries( - [ - entry(prefill_workers=1, decode_workers=1, conc=32), - entry(prefill_workers=2, decode_workers=1, conc=64), - entry(prefill_workers=1, decode_workers=2, conc=128), - ] - ) + result = mark_eval_entries([ + entry(prefill_workers=1, decode_workers=1, conc=32), + entry(prefill_workers=2, decode_workers=1, conc=64), + entry(prefill_workers=1, decode_workers=2, conc=128), + ]) assert [(e["run-eval"], e["eval-conc"]) for e in result] == [ (True, 32), @@ -740,111 +699,50 @@ def test_multi_node_split_parallelism_uses_only_highest_concurrency_entry(self): def test_marks_highest_and_median_conc(self): """Should mark highest and median concurrency for 8k1k entries.""" entries = [ - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "tp": 2, - "conc": 32, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - }, - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "tp": 2, - "conc": 128, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - }, - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "tp": 2, - "conc": 512, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - }, + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 32, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 128, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 512, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, ] result = mark_eval_entries(entries) # conc values: [32, 128, 512]. median=128 (index 1), highest=512 - assert result[0]["run-eval"] is False # conc=32 - assert result[1]["run-eval"] is True # conc=128 (median) - assert result[2]["run-eval"] is True # conc=512 (highest) + assert result[0]['run-eval'] is False # conc=32 + assert result[1]['run-eval'] is True # conc=128 (median) + assert result[2]['run-eval'] is True # conc=512 (highest) def test_non_8k1k_never_marked(self): """Entries with non-8k1k seq lengths should never be eval-marked.""" entries = [ - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 1024, - "osl": 1024, - "tp": 2, - "conc": 512, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - }, + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 512, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, ] result = mark_eval_entries(entries) - assert result[0]["run-eval"] is False + assert result[0]['run-eval'] is False def test_never_marks_all_entries(self): """mark_eval_entries should never mark every single-node entry, ensuring the e2e splitting logic can distinguish default from evals-only.""" entries = [ - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "tp": 2, - "conc": c, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - } + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': c, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False} for c in [32, 64, 128, 256, 512] ] + [ # Non-8k1k entry that should never be marked - { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 1024, - "osl": 1024, - "tp": 2, - "conc": 64, - "spec-decoding": False, - "dp-attn": False, - "run-eval": False, - }, + {'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 64, + 'spec-decoding': False, 'dp-attn': False, 'run-eval': False}, ] result = mark_eval_entries(entries) - non_prefill = [x for x in result if "prefill" not in x] - assert not all(x["run-eval"] for x in non_prefill), ( + non_prefill = [x for x in result if 'prefill' not in x] + assert not all(x['run-eval'] for x in non_prefill), \ "mark_eval_entries must not mark all entries — would break e2e splitting" - ) class TestMarkAllEvalEntries: @@ -853,202 +751,150 @@ class TestMarkAllEvalEntries: def test_marks_only_8k1k_entries_and_passes_other_seq_lens_through(self): entries = [ { # 1k1k is not eligible for evals -> left unmarked - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 1024, - "osl": 1024, - "tp": 2, - "conc": 1, - "spec-decoding": "none", - "dp-attn": False, - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 1024, 'osl': 1024, 'tp': 2, 'conc': 1, + 'spec-decoding': 'none', 'dp-attn': False, 'run-eval': False, }, { # 8k1k is eligible -> marked for eval - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "tp": 2, - "conc": 8, - "spec-decoding": "none", - "dp-attn": False, - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'tp': 2, 'conc': 8, + 'spec-decoding': 'none', 'dp-attn': False, 'run-eval': False, }, ] result = mark_all_eval_entries(entries) - by_isl = {entry["isl"]: entry for entry in result} - assert by_isl[1024]["run-eval"] is False - assert by_isl[8192]["run-eval"] is True + by_isl = {entry['isl']: entry for entry in result} + assert by_isl[1024]['run-eval'] is False + assert by_isl[8192]['run-eval'] is True def test_batches_every_multinode_concurrency_per_engine_topology(self): entries = [ { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [1, 4, 8, 16], - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [1, 4, 8, 16], + 'run-eval': False, }, { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": True}, - "decode": {"dp-attn": False}, - "conc": [32], - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': True}, + 'decode': {'dp-attn': False}, + 'conc': [32], + 'run-eval': False, }, ] result = mark_all_eval_entries(entries) assert len(result) == 2 - assert all(entry["run-eval"] for entry in result) - assert [entry["conc"] for entry in result] == [ - [1, 4, 8, 16], - [32], + assert all(entry['run-eval'] for entry in result) + assert [entry['conc'] for entry in result] == [ + [1, 4, 8, 16], [32], ] - assert all(entry["eval-all-concs"] is True for entry in result) - assert all("eval-conc" not in entry for entry in result) + assert all(entry['eval-all-concs'] is True for entry in result) + assert all('eval-conc' not in entry for entry in result) def test_default_eval_selection_does_not_collapse_all_evals_expansion(self): entries = [ { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [1, 4, 8, 16, 32], - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [1, 4, 8, 16, 32], + 'run-eval': False, }, ] result = mark_all_eval_entries(mark_eval_entries(entries)) assert len(result) == 1 - assert result[0]["conc"] == [1, 4, 8, 16, 32] - assert result[0]["eval-all-concs"] is True - assert "eval-conc" not in result[0] - assert result[0]["run-eval"] is True + assert result[0]['conc'] == [1, 4, 8, 16, 32] + assert result[0]['eval-all-concs'] is True + assert 'eval-conc' not in result[0] + assert result[0]['run-eval'] is True def test_deduplicates_overlapping_concurrency_rows_for_same_parallelism(self): entries = [ { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [4, 8, 16], - "run-eval": False, - "eval-conc": None, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [4, 8, 16], + 'run-eval': False, + 'eval-conc': None, }, { - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [16, 32], - "run-eval": True, - "eval-conc": 32, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [16, 32], + 'run-eval': True, + 'eval-conc': 32, }, ] result = mark_all_eval_entries(entries) assert len(result) == 1 - assert result[0]["conc"] == [4, 8, 16, 32] - assert result[0]["eval-all-concs"] is True - assert "eval-conc" not in result[0] + assert result[0]['conc'] == [4, 8, 16, 32] + assert result[0]['eval-all-concs'] is True + assert 'eval-conc' not in result[0] def test_excludes_1k1k_multinode_entries_from_expansion(self): entries = [ { # 1k1k multinode: left untouched, never batched or eval-marked - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 1024, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [4, 8, 16], - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 1024, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [4, 8, 16], + 'run-eval': False, }, { # 8k1k multinode: expanded into a batched eval row - "model": "m", - "runner": "r", - "framework": "f", - "precision": "fp8", - "isl": 8192, - "osl": 1024, - "spec-decoding": "none", - "prefill": {"dp-attn": False}, - "decode": {"dp-attn": False}, - "conc": [8, 32], - "run-eval": False, + 'model': 'm', 'runner': 'r', 'framework': 'f', 'precision': 'fp8', + 'isl': 8192, 'osl': 1024, 'spec-decoding': 'none', + 'prefill': {'dp-attn': False}, + 'decode': {'dp-attn': False}, + 'conc': [8, 32], + 'run-eval': False, }, ] result = mark_all_eval_entries(entries) assert len(result) == 2 - one_k = next(e for e in result if e["isl"] == 1024) - eight_k = next(e for e in result if e["isl"] == 8192) + one_k = next(e for e in result if e['isl'] == 1024) + eight_k = next(e for e in result if e['isl'] == 8192) # 1k1k untouched: not eval-marked, not batched, concurrency unchanged - assert one_k["run-eval"] is False - assert "eval-all-concs" not in one_k - assert one_k["conc"] == [4, 8, 16] + assert one_k['run-eval'] is False + assert 'eval-all-concs' not in one_k + assert one_k['conc'] == [4, 8, 16] # 8k1k expanded into a batched eval row - assert eight_k["run-eval"] is True - assert eight_k["eval-all-concs"] is True - assert eight_k["conc"] == [8, 32] + assert eight_k['run-eval'] is True + assert eight_k['eval-all-concs'] is True + assert eight_k['conc'] == [8, 32] def test_marks_agentic_entries_for_gsm8k(self): entries = [ { - "scenario-type": "agentic-coding", - "model": "m", - "runner": "r", - "conc": 64, + 'scenario-type': 'agentic-coding', + 'model': 'm', + 'runner': 'r', + 'conc': 64, } ] result = mark_all_eval_entries(entries) - assert result[0]["run-eval"] is True - assert "eval-conc" not in result[0] + assert result[0]['run-eval'] is True + assert 'eval-conc' not in result[0] def test_marks_multinode_agentic_entries_for_swebench(self): """Unlike fixed-seq-len multi-node (which batches every concurrency @@ -1056,69 +902,55 @@ def test_marks_multinode_agentic_entries_for_swebench(self): the same topology are merged but only their highest conc is marked via eval-conc, since SWE-bench doesn't support batched concurrencies.""" common = { - "scenario-type": "agentic-coding", - "model": "m", - "runner": "r", - "framework": "sglang-disagg", - "precision": "fp4", - "spec-decoding": "none", - "disagg": True, - "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, - "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + 'scenario-type': 'agentic-coding', + 'model': 'm', 'runner': 'r', 'framework': 'sglang-disagg', + 'precision': 'fp4', 'spec-decoding': 'none', 'disagg': True, + 'prefill': {'num-worker': 1, 'tp': 8, 'ep': 1, 'dp-attn': False}, + 'decode': {'num-worker': 1, 'tp': 8, 'ep': 1, 'dp-attn': False}, } entries = [ - {**common, "conc": [2], "exp-name": "p1x8_d1x8_conc2"}, - {**common, "conc": [16], "exp-name": "p1x8_d1x8_conc16"}, - {**common, "conc": [32], "exp-name": "p1x8_d1x8_conc32"}, + {**common, 'conc': [2], 'exp-name': 'p1x8_d1x8_conc2'}, + {**common, 'conc': [16], 'exp-name': 'p1x8_d1x8_conc16'}, + {**common, 'conc': [32], 'exp-name': 'p1x8_d1x8_conc32'}, ] result = mark_all_eval_entries(entries) assert len(result) == 1 - assert result[0]["run-eval"] is True - assert result[0]["conc"] == [2, 16, 32] - assert result[0]["eval-conc"] == 32 - assert "eval-all-concs" not in result[0] + assert result[0]['run-eval'] is True + assert result[0]['conc'] == [2, 16, 32] + assert result[0]['eval-conc'] == 32 + assert 'eval-all-concs' not in result[0] # ============================================================================= # Test generate_full_sweep for single-node # ============================================================================= - class TestGenerateFullSweepSingleNode: """Tests for generate_full_sweep with single-node configs.""" - def test_basic_sweep_generation( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_basic_sweep_generation(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Basic single-node sweep should generate entries.""" result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) > 0 # With step_size=2, conc goes 4, 8, 16, 32, 64 = 5 values per seq-len config # 2 seq-len configs * 5 = 10 entries assert len(result) == 10 - def test_matrix_entry_structure( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_matrix_entry_structure(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Generated entries should have correct structure.""" result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) entry = result[0] - assert ( - entry["image"] - == "rocm/7.0:rocm7.0_ubuntu_22.04_sgl-dev-v0.5.2-rocm7.0-mi30x-20250915" - ) + assert entry["image"] == "rocm/7.0:rocm7.0_ubuntu_22.04_sgl-dev-v0.5.2-rocm7.0-mi30x-20250915" assert entry["model"] == "deepseek-ai/DeepSeek-R1-0528" assert entry["precision"] == "fp8" assert entry["framework"] == "sglang" @@ -1129,9 +961,7 @@ def test_matrix_entry_structure( assert (entry["pp"], entry["dcp-size"], entry["pcp-size"]) == (1, 1, 1) explicit_config = copy.deepcopy(sample_single_node_config) - for seq_config in explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"][ - "fixed-seq-len" - ]: + for seq_config in explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"]: for search_entry in seq_config["search-space"]: search_entry.update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) explicit_result = generate_full_sweep( @@ -1140,163 +970,141 @@ def test_matrix_entry_structure( sample_runner_config, ) assert { - (row["pp"], row["dcp-size"], row["pcp-size"]) for row in explicit_result + (row["pp"], row["dcp-size"], row["pcp-size"]) + for row in explicit_result } == {(2, 2, 2)} - def test_filter_by_model_prefix( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_filter_by_model_prefix(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by model prefix should work.""" full_sweep_args_single_node.model_prefix = ["dsr1"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) > 0 # Non-matching prefix should return empty full_sweep_args_single_node.model_prefix = ["nonexistent"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) == 0 - def test_filter_by_precision( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_filter_by_precision(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by precision should work.""" full_sweep_args_single_node.precision = ["fp8"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.precision = ["fp4"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) == 0 - def test_filter_by_framework( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_filter_by_framework(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by framework should work.""" full_sweep_args_single_node.framework = ["sglang"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.framework = ["vllm"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) == 0 - def test_filter_by_runner_type( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_filter_by_runner_type(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by runner type should work.""" full_sweep_args_single_node.runner_type = ["mi300x"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) > 0 full_sweep_args_single_node.runner_type = ["h100"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert len(result) == 0 - def test_invalid_runner_type_raises_error( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_invalid_runner_type_raises_error(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Invalid runner type should raise ValueError.""" full_sweep_args_single_node.runner_type = ["invalid_runner"] with pytest.raises(ValueError) as exc_info: generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config, + sample_runner_config ) assert "Invalid runner type" in str(exc_info.value) - def test_filter_by_seq_lens( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_filter_by_seq_lens(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by sequence lengths should work.""" full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # Only 1k1k entries, 5 concurrency values assert len(result) == 5 assert all(entry["isl"] == 1024 and entry["osl"] == 1024 for entry in result) - def test_max_conc_filter( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_conc_filter(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """max_conc filter should limit concurrency values.""" full_sweep_args_single_node.max_conc = 16 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # conc values: 4, 8, 16 (32, 64 filtered out) assert len(result) == 3 assert all(entry["conc"] <= 16 for entry in result) - def test_max_conc_creates_config_when_below_min( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_conc_creates_config_when_below_min(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """max_conc below config's min should create config with max_conc value.""" # Config has conc-start=4, so max_conc=1 should create entry with conc=1 full_sweep_args_single_node.max_conc = 1 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # Should create 1 entry with conc=1 assert len(result) == 1 assert result[0]["conc"] == 1 - def test_max_conc_zero_or_negative_skips( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_conc_zero_or_negative_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """max_conc of 0 or negative should skip configs.""" for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_conc = invalid_value result = generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config, + sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_conc={invalid_value}" @@ -1313,20 +1121,13 @@ def test_max_tp_filter(self, sample_runner_config, full_sweep_args_single_node): "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ - { - "tp": 4, - "conc-start": 4, - "conc-end": 64, - }, # should remain - { - "tp": 8, - "conc-start": 4, - "conc-end": 64, - }, # should be skipped + {"tp": 4, "conc-start": 4, "conc-end": 64}, # should remain + {"tp": 8, "conc-start": 4, "conc-end": 64}, # should be skipped ], } ] @@ -1347,12 +1148,7 @@ def test_max_tp_filter(self, sample_runner_config, full_sweep_args_single_node): assert len(result) == 5 assert all(entry["tp"] == 4 for entry in result) - def test_max_tp_below_all_available_skips( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_tp_below_all_available_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """If all available tp values are > max_tp, generator should return empty (skip).""" full_sweep_args_single_node.max_tp = 2 full_sweep_args_single_node.seq_lens = ["1k1k"] @@ -1365,33 +1161,25 @@ def test_max_tp_below_all_available_skips( assert len(result) == 0 - def test_max_tp_zero_or_negative_skips( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_tp_zero_or_negative_skips(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """max_tp of 0 or negative should skip configs.""" for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_tp = invalid_value result = generate_full_sweep( full_sweep_args_single_node, sample_single_node_config, - sample_runner_config, + sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_tp={invalid_value}" - def test_step_size( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_step_size(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Different step sizes should affect concurrency progression.""" full_sweep_args_single_node.step_size = 4 full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # conc: 4, 16, 64 = 3 values assert len(result) == 3 @@ -1400,48 +1188,37 @@ def test_step_size( assert 16 in conc_values assert 64 in conc_values - def test_exp_name_format( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_exp_name_format(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """exp-name should have correct format.""" full_sweep_args_single_node.seq_lens = ["1k1k"] result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) assert all(entry["exp-name"] == "dsr1_1k1k" for entry in result) - def test_max_model_len_calculation( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_max_model_len_calculation(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """max-model-len should be isl + osl + 256.""" result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) for entry in result: expected_max_model_len = entry["isl"] + entry["osl"] + 256 assert entry["max-model-len"] == expected_max_model_len - def test_runner_node_filter( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_runner_node_filter(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Runner node filter should expand entries to individual matching nodes.""" full_sweep_args_single_node.runner_type = ["mi300x"] full_sweep_args_single_node.runner_node_filter = "amd" full_sweep_args_single_node.seq_lens = ["1k1k"] - full_sweep_args_single_node.max_conc = ( - 4 # Limit to single conc value for easier counting - ) + full_sweep_args_single_node.max_conc = 4 # Limit to single conc value for easier counting result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # 2 amd nodes (mi300x-amd_0, mi300x-amd_1), 1 conc value = 2 entries assert len(result) == 2 @@ -1450,62 +1227,56 @@ def test_runner_node_filter( assert "mi300x-amd_0" in runners assert "mi300x-amd_1" in runners - def test_runner_node_filter_no_match( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_runner_node_filter_no_match(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Runner node filter with no matches should skip configs (return empty).""" full_sweep_args_single_node.runner_type = ["mi300x"] full_sweep_args_single_node.runner_node_filter = "nonexistent" result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # No nodes match, so config is skipped assert len(result) == 0 - def test_runner_node_filter_without_runner_type( - self, - sample_single_node_config, - sample_runner_config, - full_sweep_args_single_node, - ): + def test_runner_node_filter_without_runner_type(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Runner node filter should work without explicit runner type (uses config's runner).""" full_sweep_args_single_node.runner_node_filter = "amd" full_sweep_args_single_node.seq_lens = ["1k1k"] full_sweep_args_single_node.max_conc = 4 result = generate_full_sweep( - full_sweep_args_single_node, sample_single_node_config, sample_runner_config + full_sweep_args_single_node, + sample_single_node_config, + sample_runner_config ) # Config has runner=mi300x, filter "amd" matches mi300x-amd_0 and mi300x-amd_1 assert len(result) == 2 assert all("amd" in entry["runner"] for entry in result) + # ============================================================================= # Test generate_full_sweep for multi-node # ============================================================================= - class TestGenerateFullSweepMultiNode: """Tests for generate_full_sweep with multi-node configs.""" - def test_multinode_sweep_generation( - self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_sweep_generation(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): """Multinode sweep should generate entries with prefill/decode.""" result = generate_full_sweep( - full_sweep_args_multi_node, sample_multinode_config, sample_runner_config + full_sweep_args_multi_node, + sample_multinode_config, + sample_runner_config ) assert len(result) == 1 # One entry with conc-list - def test_multinode_entry_structure( - self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_entry_structure(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): """Multinode entries should have prefill and decode configs.""" result = generate_full_sweep( - full_sweep_args_multi_node, sample_multinode_config, sample_runner_config + full_sweep_args_multi_node, + sample_multinode_config, + sample_runner_config ) entry = result[0] assert "prefill" in entry @@ -1526,13 +1297,9 @@ def test_multinode_entry_structure( entry["decode"]["pcp-size"], ) == (1, 1, 1) - def test_multinode_parallelism_fields( - self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_parallelism_fields(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): explicit_config = copy.deepcopy(sample_multinode_config) - search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"][ - "fixed-seq-len" - ][0]["search-space"][0] + search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0]["search-space"][0] search_entry["prefill"].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) search_entry["decode"].update({"pp": 2, "dcp-size": 4, "pcp-size": 1}) @@ -1553,29 +1320,27 @@ def test_multinode_parallelism_fields( entry["decode"]["pcp-size"], ) == (2, 4, 1) - def test_multinode_conc_as_list( - self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_conc_as_list(self, sample_multinode_config, sample_runner_config, full_sweep_args_multi_node): """Multinode conc should be passed as list.""" result = generate_full_sweep( - full_sweep_args_multi_node, sample_multinode_config, sample_runner_config + full_sweep_args_multi_node, + sample_multinode_config, + sample_runner_config ) entry = result[0] assert isinstance(entry["conc"], list) assert entry["conc"] == [2150] - def test_single_node_flag_skips_multinode( - self, sample_multinode_config, sample_runner_config, full_sweep_args_single_node - ): + def test_single_node_flag_skips_multinode(self, sample_multinode_config, sample_runner_config, full_sweep_args_single_node): """Single-node flag should skip multinode configs.""" result = generate_full_sweep( - full_sweep_args_single_node, sample_multinode_config, sample_runner_config + full_sweep_args_single_node, + sample_multinode_config, + sample_runner_config ) assert len(result) == 0 - def test_runner_node_filter_multinode( - self, sample_runner_config, full_sweep_args_multi_node - ): + def test_runner_node_filter_multinode(self, sample_runner_config, full_sweep_args_multi_node): """Runner node filter should work with multinode configs.""" # Create a multinode config with h200 runner (which has 4 nodes) config = { @@ -1591,6 +1356,7 @@ def test_runner_node_filter_multinode( "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, @@ -1610,16 +1376,18 @@ def test_runner_node_filter_multinode( "dp-attn": False, }, } - ], + ] } ] - }, + } } } full_sweep_args_multi_node.runner_type = ["h200"] full_sweep_args_multi_node.runner_node_filter = "cw" result = generate_full_sweep( - full_sweep_args_multi_node, config, sample_runner_config + full_sweep_args_multi_node, + config, + sample_runner_config ) # Only h200-cw_0 and h200-cw_1 match "cw" filter assert len(result) == 2 @@ -1633,13 +1401,10 @@ def test_runner_node_filter_multinode( # Test edge cases and special configurations # ============================================================================= - class TestEdgeCases: """Tests for edge cases and special configurations.""" - def test_config_with_ep_and_dp_attn( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_config_with_ep_and_dp_attn(self, sample_runner_config, full_sweep_args_single_node): """Config with ep and dp-attn should be handled correctly.""" config = { "test-config": { @@ -1652,33 +1417,28 @@ def test_config_with_ep_and_dp_attn( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ - { - "tp": 4, - "ep": 4, - "dp-attn": True, - "conc-start": 4, - "conc-end": 4, - } - ], + {"tp": 4, "ep": 4, "dp-attn": True, "conc-start": 4, "conc-end": 4} + ] } ] - }, + } } } result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) assert len(result) == 1 assert result[0]["ep"] == 4 assert result[0]["dp-attn"] is True - def test_config_with_spec_decoding( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_config_with_spec_decoding(self, sample_runner_config, full_sweep_args_single_node): """Config with spec-decoding should be handled correctly.""" config = { "test-config": { @@ -1691,31 +1451,27 @@ def test_config_with_spec_decoding( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ - { - "tp": 8, - "spec-decoding": "mtp", - "conc-start": 4, - "conc-end": 4, - } - ], + {"tp": 8, "spec-decoding": "mtp", "conc-start": 4, "conc-end": 4} + ] } ] - }, + } } } result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) assert len(result) == 1 assert result[0]["spec-decoding"] == "mtp" - def test_conc_list_in_single_node( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_conc_list_in_single_node(self, sample_runner_config, full_sweep_args_single_node): """Single node config with conc-list should work.""" config = { "test-config": { @@ -1728,17 +1484,22 @@ def test_conc_list_in_single_node( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, - "search-space": [{"tp": 8, "conc-list": [4, 16, 64]}], + "search-space": [ + {"tp": 8, "conc-list": [4, 16, 64]} + ] } ] - }, + } } } result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) conc_values = [entry["conc"] for entry in result] assert conc_values == [4, 16, 64] @@ -1762,7 +1523,9 @@ def test_conc_list_in_single_node_honors_filters( { "isl": 1024, "osl": 1024, - "search-space": [{"tp": 8, "conc-list": [4, 16, 64]}], + "search-space": [ + {"tp": 8, "conc-list": [4, 16, 64]} + ], } ] }, @@ -1810,9 +1573,7 @@ def test_min_conc_cannot_exceed_max_conc( sample_runner_config, ) - def test_disagg_defaults_to_false( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_disagg_defaults_to_false(self, sample_runner_config, full_sweep_args_single_node): """disagg should default to False when not specified.""" config = { "test-config": { @@ -1826,23 +1587,26 @@ def test_disagg_defaults_to_false( # No disagg field "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, - "search-space": [{"tp": 8, "conc-start": 4, "conc-end": 4}], + "search-space": [ + {"tp": 8, "conc-start": 4, "conc-end": 4} + ] } ] - }, + } } } result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) assert result[0]["disagg"] is False - def test_multinode_conc_range_expansion( - self, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_conc_range_expansion(self, sample_runner_config, full_sweep_args_multi_node): """Multinode with conc range should expand to list.""" config = { "test-config": { @@ -1857,6 +1621,7 @@ def test_multinode_conc_range_expansion( "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, @@ -1877,22 +1642,22 @@ def test_multinode_conc_range_expansion( "dp-attn": False, }, } - ], + ] } ] - }, + } } } result = generate_full_sweep( - full_sweep_args_multi_node, config, sample_runner_config + full_sweep_args_multi_node, + config, + sample_runner_config ) assert len(result) == 1 # step_size=2: 1, 2, 4, 8 assert result[0]["conc"] == [1, 2, 4, 8] - def test_max_ep_creates_config_when_below_min( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_max_ep_creates_config_when_below_min(self, sample_runner_config, full_sweep_args_single_node): """max_ep below config's ep should create config with max_ep value.""" config = { "test-config": { @@ -1905,28 +1670,29 @@ def test_max_ep_creates_config_when_below_min( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ {"tp": 8, "ep": 8, "conc-start": 4, "conc-end": 4} - ], + ] } ] - }, + } } } full_sweep_args_single_node.max_ep = 2 result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) # ep=8 in config, but max_ep=2, so should use ep=2 assert len(result) == 1 assert result[0]["ep"] == 2 - def test_max_ep_zero_or_negative_skips( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_max_ep_zero_or_negative_skips(self, sample_runner_config, full_sweep_args_single_node): """max_ep of 0 or negative should skip configs.""" config = { "test-config": { @@ -1939,27 +1705,28 @@ def test_max_ep_zero_or_negative_skips( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ {"tp": 8, "ep": 8, "conc-start": 4, "conc-end": 4} - ], + ] } ] - }, + } } } for invalid_value in [0, -1, -100]: full_sweep_args_single_node.max_ep = invalid_value result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_ep={invalid_value}" - def test_multinode_max_conc_zero_or_negative_skips( - self, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_max_conc_zero_or_negative_skips(self, sample_runner_config, full_sweep_args_multi_node): """Multinode max_conc of 0 or negative should skip configs.""" config = { "test-config": { @@ -1974,6 +1741,7 @@ def test_multinode_max_conc_zero_or_negative_skips( "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, @@ -1993,22 +1761,22 @@ def test_multinode_max_conc_zero_or_negative_skips( "dp-attn": False, }, } - ], + ] } ] - }, + } } } for invalid_value in [0, -1, -100]: full_sweep_args_multi_node.max_conc = invalid_value result = generate_full_sweep( - full_sweep_args_multi_node, config, sample_runner_config + full_sweep_args_multi_node, + config, + sample_runner_config ) assert len(result) == 0, f"Expected 0 results for max_conc={invalid_value}" - def test_multinode_max_conc_creates_config_when_below_min( - self, sample_runner_config, full_sweep_args_multi_node - ): + def test_multinode_max_conc_creates_config_when_below_min(self, sample_runner_config, full_sweep_args_multi_node): """Multinode max_conc below all values should create config with max_conc.""" config = { "test-config": { @@ -2023,6 +1791,7 @@ def test_multinode_max_conc_creates_config_when_below_min( "kv-p2p-transfer": "nixl", "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, @@ -2042,23 +1811,23 @@ def test_multinode_max_conc_creates_config_when_below_min( "dp-attn": False, }, } - ], + ] } ] - }, + } } } full_sweep_args_multi_node.max_conc = 1 result = generate_full_sweep( - full_sweep_args_multi_node, config, sample_runner_config + full_sweep_args_multi_node, + config, + sample_runner_config ) # All conc values (100, 200, 400) > max_conc (1), so should use [1] assert len(result) == 1 assert result[0]["conc"] == [1] - def test_combined_max_filters( - self, sample_runner_config, full_sweep_args_single_node - ): + def test_combined_max_filters(self, sample_runner_config, full_sweep_args_single_node): """Multiple max filters should all apply (tp skip, ep clamp, conc clamp).""" config = { "test-config": { @@ -2071,26 +1840,17 @@ def test_combined_max_filters( "multinode": False, "scenarios": { "fixed-seq-len": [ + { "isl": 1024, "osl": 1024, "search-space": [ - { - "tp": 8, - "ep": 8, - "conc-start": 100, - "conc-end": 200, - }, # should be skipped - { - "tp": 2, - "ep": 8, - "conc-start": 100, - "conc-end": 200, - }, # should remain - ], + {"tp": 8, "ep": 8, "conc-start": 100, "conc-end": 200}, # should be skipped + {"tp": 2, "ep": 8, "conc-start": 100, "conc-end": 200}, # should remain + ] } ] - }, + } } } full_sweep_args_single_node.max_tp = 2 @@ -2098,7 +1858,9 @@ def test_combined_max_filters( full_sweep_args_single_node.max_conc = 1 result = generate_full_sweep( - full_sweep_args_single_node, config, sample_runner_config + full_sweep_args_single_node, + config, + sample_runner_config ) assert len(result) == 1 @@ -2106,12 +1868,10 @@ def test_combined_max_filters( assert result[0]["ep"] == 1 assert result[0]["conc"] == 1 - # ============================================================================= # Test argument parsing and defaults # ============================================================================= - class TestArgumentDefaults: """Tests for command-line argument parsing and default values.""" @@ -2126,11 +1886,10 @@ def test_runner_config_default_value(self): try: # Simulate command-line args without --runner-config flag sys.argv = [ - "generate_sweep_configs.py", - "full-sweep", - "--config-files", - "dummy.yaml", - "--single-node", + 'generate_sweep_configs.py', + 'full-sweep', + '--config-files', 'dummy.yaml', + '--single-node' ] # Parse args using the ArgumentParser from main @@ -2141,44 +1900,44 @@ def test_runner_config_default_value(self): # Create the same parent parser as in main() parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - "--config-files", - nargs="+", + '--config-files', + nargs='+', required=True, - help="One or more configuration files (YAML format)", + help='One or more configuration files (YAML format)' ) parent_parser.add_argument( - "--runner-config", - default="configs/runners.yaml", - help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", + '--runner-config', + default='configs/runners.yaml', + help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' ) # Create main parser parser = argparse.ArgumentParser( - description="Generate benchmark configurations from YAML config files" + description='Generate benchmark configurations from YAML config files' ) # Create subparsers subparsers = parser.add_subparsers( - dest="command", required=True, help="Available commands" + dest='command', + required=True, + help='Available commands' ) # Add full-sweep subparser full_sweep_parser = subparsers.add_parser( - "full-sweep", + 'full-sweep', parents=[parent_parser], add_help=False, - help="Generate full sweep configurations", + help='Generate full sweep configurations' ) - full_sweep_parser.add_argument("--single-node", action="store_true") - full_sweep_parser.add_argument("--multi-node", action="store_true") + full_sweep_parser.add_argument('--single-node', action='store_true') + full_sweep_parser.add_argument('--multi-node', action='store_true') # Parse the args - args = parser.parse_args( - ["full-sweep", "--config-files", "dummy.yaml", "--single-node"] - ) + args = parser.parse_args(['full-sweep', '--config-files', 'dummy.yaml', '--single-node']) # Verify the default value - assert args.runner_config == "configs/runners.yaml" + assert args.runner_config == 'configs/runners.yaml' finally: # Restore original sys.argv @@ -2191,50 +1950,48 @@ def test_runner_config_explicit_value(self): # Create the same parent parser as in main() parent_parser = argparse.ArgumentParser(add_help=False) parent_parser.add_argument( - "--config-files", - nargs="+", + '--config-files', + nargs='+', required=True, - help="One or more configuration files (YAML format)", + help='One or more configuration files (YAML format)' ) parent_parser.add_argument( - "--runner-config", - default="configs/runners.yaml", - help="Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)", + '--runner-config', + default='configs/runners.yaml', + help='Configuration file holding runner information (YAML format, defaults to configs/runners.yaml)' ) # Create main parser parser = argparse.ArgumentParser( - description="Generate benchmark configurations from YAML config files" + description='Generate benchmark configurations from YAML config files' ) # Create subparsers subparsers = parser.add_subparsers( - dest="command", required=True, help="Available commands" + dest='command', + required=True, + help='Available commands' ) # Add full-sweep subparser full_sweep_parser = subparsers.add_parser( - "full-sweep", + 'full-sweep', parents=[parent_parser], add_help=False, - help="Generate full sweep configurations", + help='Generate full sweep configurations' ) - full_sweep_parser.add_argument("--single-node", action="store_true") + full_sweep_parser.add_argument('--single-node', action='store_true') # Parse with explicit --runner-config - args = parser.parse_args( - [ - "full-sweep", - "--config-files", - "dummy.yaml", - "--runner-config", - "custom/path/runners.yaml", - "--single-node", - ] - ) + args = parser.parse_args([ + 'full-sweep', + '--config-files', 'dummy.yaml', + '--runner-config', 'custom/path/runners.yaml', + '--single-node' + ]) # Verify the explicit value - assert args.runner_config == "custom/path/runners.yaml" + assert args.runner_config == 'custom/path/runners.yaml' def test_all_evals_cli_marks_every_fixed_sequence_entry( self, @@ -2249,39 +2006,33 @@ def test_all_evals_cli_marks_every_fixed_sequence_entry( monkeypatch.setattr( generate_sweep_configs, - "load_config_files", + 'load_config_files', lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - "load_runner_file", + 'load_runner_file', lambda _: sample_runner_config, ) - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "test-config", - "--config-files", - "dummy.yaml", - "--config-keys", - "dsr1-fp8-mi300x-sglang", - "--all-evals", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dsr1-fp8-mi300x-sglang', + '--all-evals', + ]) result = generate_sweep_configs.main() # Every 8k1k concurrency is marked (5 conc values), and the 1k1k # entries are dropped rather than evaluated. assert len(result) == 5 - assert {(entry["isl"], entry["osl"]) for entry in result} == { + assert {(entry['isl'], entry['osl']) for entry in result} == { (8192, 1024), } - assert min(entry["conc"] for entry in result) == 4 - assert all(entry["run-eval"] is True for entry in result) - assert all(entry["eval-only"] is True for entry in result) + assert min(entry['conc'] for entry in result) == 4 + assert all(entry['run-eval'] is True for entry in result) + assert all(entry['eval-only'] is True for entry in result) def test_all_evals_composes_with_evals_only( self, @@ -2294,37 +2045,31 @@ def test_all_evals_composes_with_evals_only( monkeypatch.setattr( generate_sweep_configs, - "load_config_files", + 'load_config_files', lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - "load_runner_file", + 'load_runner_file', lambda _: sample_runner_config, ) - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "test-config", - "--config-files", - "dummy.yaml", - "--config-keys", - "dsr1-fp8-mi300x-sglang", - "--evals-only", - "--all-evals", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dsr1-fp8-mi300x-sglang', + '--evals-only', + '--all-evals', + ]) result = generate_sweep_configs.main() assert len(result) == 5 - assert {(entry["isl"], entry["osl"]) for entry in result} == { + assert {(entry['isl'], entry['osl']) for entry in result} == { (8192, 1024), } - assert all(entry["run-eval"] is True for entry in result) - assert all(entry["eval-only"] is True for entry in result) + assert all(entry['run-eval'] is True for entry in result) + assert all(entry['eval-only'] is True for entry in result) def test_trim_conc_reduces_generated_eval_matrix( self, @@ -2337,56 +2082,50 @@ def test_trim_conc_reduces_generated_eval_matrix( monkeypatch.setattr( generate_sweep_configs, - "load_config_files", + 'load_config_files', lambda _: sample_single_node_config, ) monkeypatch.setattr( generate_sweep_configs, - "load_runner_file", + 'load_runner_file', lambda _: sample_runner_config, ) - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "test-config", - "--config-files", - "dummy.yaml", - "--config-keys", - "dsr1-fp8-mi300x-sglang", - "--evals-only", - "--all-evals", - "--trim-conc", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dsr1-fp8-mi300x-sglang', + '--evals-only', + '--all-evals', + '--trim-conc', + ]) result = generate_sweep_configs.main() assert len(result) == 1 - assert result[0]["conc"] == 4 - assert result[0]["run-eval"] is True - assert result[0]["eval-only"] is True + assert result[0]['conc'] == 4 + assert result[0]['run-eval'] is True + assert result[0]['eval-only'] is True def test_trim_conc_updates_multinode_dispatch_concurrency(self): low_entry = { - "prefill": {"num-worker": 1, "tp": 8}, - "decode": {"num-worker": 0, "tp": 8}, - "conc": [4], + 'prefill': {'num-worker': 1, 'tp': 8}, + 'decode': {'num-worker': 0, 'tp': 8}, + 'conc': [4], } high_entry = { **low_entry, - "conc": [64], - "run-eval": True, - "eval-conc": 64, + 'conc': [64], + 'run-eval': True, + 'eval-conc': 64, } result = trim_conc([high_entry, low_entry]) assert len(result) == 1 - assert result[0]["conc"] == [4] - assert result[0]["eval-conc"] == 4 - assert result[0]["run-eval"] is True + assert result[0]['conc'] == [4] + assert result[0]['eval-conc'] == 4 + assert result[0]['run-eval'] is True def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( self, @@ -2396,47 +2135,43 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( import generate_sweep_configs repo_root = Path(__file__).resolve().parents[2] - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "full-sweep", - "--config-files", - str(repo_root / "configs/nvidia-master.yaml"), - str(repo_root / "configs/amd-master.yaml"), - "--runner-config", - str(repo_root / "configs/runners.yaml"), - "--model-prefix", - "kimik3", - "minimaxm3", - "--scenario-type", - "agentic-coding", - "--evals-only", - "--all-evals", - "--trim-conc", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'full-sweep', + '--config-files', + str(repo_root / 'configs/nvidia-master.yaml'), + str(repo_root / 'configs/amd-master.yaml'), + '--runner-config', + str(repo_root / 'configs/runners.yaml'), + '--model-prefix', + 'kimik3', + 'minimaxm3', + '--scenario-type', + 'agentic-coding', + '--evals-only', + '--all-evals', + '--trim-conc', + ]) rows = generate_sweep_configs.main() manifest_fields = ( - "model-prefix", - "runner", - "framework", - "precision", - "tp", - "pp", - "dcp-size", - "pcp-size", - "ep", - "dp-attn", - "prefill", - "decode", - "disagg", - "kv-offloading", - "kv-offload-backend", - "spec-decoding", - "exp-name", + 'model-prefix', + 'runner', + 'framework', + 'precision', + 'tp', + 'pp', + 'dcp-size', + 'pcp-size', + 'ep', + 'dp-attn', + 'prefill', + 'decode', + 'disagg', + 'kv-offloading', + 'kv-offload-backend', + 'spec-decoding', + 'exp-name', ) manifest = sorted( tuple( @@ -2449,18 +2184,18 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( for row in rows ) manifest_digest = hashlib.sha256( - json.dumps(manifest, separators=(",", ":")).encode() + json.dumps(manifest, separators=(',', ':')).encode() ).hexdigest() assert len(manifest) == 67 assert manifest_digest == ( - "b94441edd3d083d3c03a6ed1f3891ee222c420614ba6a2a68802d9a85c128ca2" + 'b94441edd3d083d3c03a6ed1f3891ee222c420614ba6a2a68802d9a85c128ca2' ), json.dumps(manifest, indent=2) for row in rows: - if isinstance(row["conc"], list): - assert row["conc"] == [row["eval-conc"]] - assert all(row["run-eval"] is True for row in rows) - assert all(row["eval-only"] is True for row in rows) + if isinstance(row['conc'], list): + assert row['conc'] == [row['eval-conc']] + assert all(row['run-eval'] is True for row in rows) + assert all(row['eval-only'] is True for row in rows) def test_all_evals_batches_each_multinode_concurrency( self, @@ -2472,64 +2207,55 @@ def test_all_evals_batches_each_multinode_concurrency( import generate_sweep_configs config = sample_multinode_config - seq_entry = config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0] + seq_entry = ( + config['dsr1-fp4-gb200-dynamo-trt']['scenarios'] + ['fixed-seq-len'][0] + ) # all-evals only evaluates 8k1k, so target that sequence length. - seq_entry["isl"] = 8192 - seq_entry["osl"] = 1024 - search_space = seq_entry["search-space"] - search_space[0]["conc-list"] = [4, 16, 64] + seq_entry['isl'] = 8192 + seq_entry['osl'] = 1024 + search_space = seq_entry['search-space'] + search_space[0]['conc-list'] = [4, 16, 64] monkeypatch.setattr( generate_sweep_configs, - "load_config_files", + 'load_config_files', lambda _: config, ) monkeypatch.setattr( generate_sweep_configs, - "load_runner_file", + 'load_runner_file', lambda _: sample_runner_config, ) - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "test-config", - "--config-files", - "dummy.yaml", - "--config-keys", - "dsr1-fp4-gb200-dynamo-trt", - "--all-evals", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dsr1-fp4-gb200-dynamo-trt', + '--all-evals', + ]) result = generate_sweep_configs.main() assert len(result) == 1 - assert result[0]["conc"] == [4, 16, 64] - assert result[0]["eval-all-concs"] is True - assert "eval-conc" not in result[0] - assert all(entry["run-eval"] is True for entry in result) - assert all(entry["eval-only"] is True for entry in result) + assert result[0]['conc'] == [4, 16, 64] + assert result[0]['eval-all-concs'] is True + assert 'eval-conc' not in result[0] + assert all(entry['run-eval'] is True for entry in result) + assert all(entry['eval-only'] is True for entry in result) def test_all_evals_cannot_combine_with_no_evals(self, monkeypatch): import sys import generate_sweep_configs - monkeypatch.setattr( - sys, - "argv", - [ - "generate_sweep_configs.py", - "test-config", - "--config-files", - "dummy.yaml", - "--config-keys", - "dummy", - "--no-evals", - "--all-evals", - ], - ) + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'test-config', + '--config-files', 'dummy.yaml', + '--config-keys', 'dummy', + '--no-evals', + '--all-evals', + ]) with pytest.raises(SystemExit): generate_sweep_configs.main() @@ -2539,7 +2265,6 @@ def test_all_evals_cannot_combine_with_no_evals(self, monkeypatch): # Mixed-mode fixtures # ============================================================================= - @pytest.fixture def sample_mixed_config(sample_single_node_config, sample_multinode_config): """Config dict containing both single-node and multinode entries.""" @@ -2573,7 +2298,6 @@ def full_sweep_args_both(): # Test generate_test_config_sweep # ============================================================================= - class TestGenerateTestConfigSweep: """Tests for exact config-key sweep generation.""" @@ -2593,18 +2317,20 @@ def test_single_node_parallelism_fields_are_generated( args, sample_single_node_config, sample_runner_config ) assert [ - (row["pp"], row["dcp-size"], row["pcp-size"]) for row in default_result + (row["pp"], row["dcp-size"], row["pcp-size"]) + for row in default_result ] == [(1, 1, 1)] explicit_config = copy.deepcopy(sample_single_node_config) - explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"][0][ - "search-space" - ][0].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) + explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"][0]["search-space"][0].update( + {"pp": 2, "dcp-size": 2, "pcp-size": 2} + ) explicit_result = generate_test_config_sweep( args, explicit_config, sample_runner_config ) assert [ - (row["pp"], row["dcp-size"], row["pcp-size"]) for row in explicit_result + (row["pp"], row["dcp-size"], row["pcp-size"]) + for row in explicit_result ] == [(2, 2, 2)] def test_multinode_parallelism_fields_are_generated( @@ -2619,15 +2345,13 @@ def test_multinode_parallelism_fields_are_generated( runner_node_filter=None, ) explicit_config = copy.deepcopy(sample_multinode_config) - search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"][ - "fixed-seq-len" - ][0]["search-space"][0] + search_entry = explicit_config["dsr1-fp4-gb200-dynamo-trt"]["scenarios"]["fixed-seq-len"][0]["search-space"][0] search_entry["prefill"].update({"pp": 2, "dcp-size": 2, "pcp-size": 2}) search_entry["decode"].update({"pp": 2, "dcp-size": 4, "pcp-size": 1}) - entry = generate_test_config_sweep(args, explicit_config, sample_runner_config)[ - 0 - ] + entry = generate_test_config_sweep( + args, explicit_config, sample_runner_config + )[0] assert ( entry["prefill"]["pp"], @@ -2640,9 +2364,7 @@ def test_multinode_parallelism_fields_are_generated( entry["decode"]["pcp-size"], ) == (2, 4, 1) - def test_runner_node_filter_expands_config_runner( - self, sample_multinode_config, sample_runner_config - ): + def test_runner_node_filter_expands_config_runner(self, sample_multinode_config, sample_runner_config): """test-config should allow targeting one concrete runner node.""" args = argparse.Namespace( config_keys=["dsr1-fp4-gb200-dynamo-trt"], @@ -2660,9 +2382,7 @@ def test_runner_node_filter_expands_config_runner( assert len(result) == 1 assert result[0]["runner"] == "gb200-nv_0" - def test_runner_node_filter_no_match_skips_config( - self, sample_multinode_config, sample_runner_config - ): + def test_runner_node_filter_no_match_skips_config(self, sample_multinode_config, sample_runner_config): """Unmatched node filters should produce no entries.""" args = argparse.Namespace( config_keys=["dsr1-fp4-gb200-dynamo-trt"], @@ -2679,9 +2399,7 @@ def test_runner_node_filter_no_match_skips_config( assert result == [] - def test_runner_node_filter_expands_agentic_config_runner( - self, sample_runner_config - ): + def test_runner_node_filter_expands_agentic_config_runner(self, sample_runner_config): """Agentic test-config entries should support concrete runner targeting.""" config = { "qwen-agentic-hicache": { @@ -2737,42 +2455,40 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [ - { - "dram-utilization": 0.80, - "search-space": [ - { - "tp": 4, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "dcp-size": 2, - "pcp-size": 1, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "dcp-size": 1, - "pcp-size": 2, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - { - "tp": 4, - "pp": 2, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - ], - } - ], + "agentic-coding": [{ + "dram-utilization": 0.80, + "search-space": [ + { + "tp": 4, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "dcp-size": 2, + "pcp-size": 1, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "dcp-size": 1, + "pcp-size": 2, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + { + "tp": 4, + "pp": 2, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + ], + }], }, }, } @@ -2787,9 +2503,7 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): result = generate_test_config_sweep(args, config, sample_runner_config) budgets = { - (entry["pp"], entry["dcp-size"], entry["pcp-size"]): entry[ - "total-cpu-dram-gb" - ] + (entry["pp"], entry["dcp-size"], entry["pcp-size"]): entry["total-cpu-dram-gb"] for entry in result } assert budgets == { @@ -2811,19 +2525,17 @@ def test_agentic_node_dram_rejects_tp_above_runner_gpus(self, sample_runner_conf "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [ - { - "dram-utilization": 0.80, - "search-space": [ - { - "tp": 4, - "kv-offloading": "dram", - "kv-offload-backend": {"name": "native"}, - "conc-list": [32], - }, - ], - } - ], + "agentic-coding": [{ + "dram-utilization": 0.80, + "search-space": [ + { + "tp": 4, + "kv-offloading": "dram", + "kv-offload-backend": {"name": "native"}, + "conc-list": [32], + }, + ], + }], }, }, } @@ -2861,26 +2573,8 @@ def test_multinode_agentic_groups_concurrencies_per_search_entry( "search-space": [ { "conc-list": [16, 32, 64, 128, 256], - "prefill": { - "hardware": "gb200", - "num-worker": 2, - "tp": 4, - "pp": 2, - "dcp-size": 2, - "pcp-size": 2, - "ep": 4, - "dp-attn": False, - }, - "decode": { - "hardware": "h100", - "num-worker": 1, - "tp": 4, - "pp": 2, - "dcp-size": 2, - "pcp-size": 1, - "ep": 1, - "dp-attn": False, - }, + "prefill": {"hardware": "gb200", "num-worker": 2, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 2, "ep": 4, "dp-attn": False}, + "decode": {"hardware": "h100", "num-worker": 1, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 1, "ep": 1, "dp-attn": False}, } ], } @@ -2928,30 +2622,16 @@ def test_multinode_agentic_preserves_kv_offload_fields(self, sample_runner_confi "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [ - { - "dram-utilization": 0.80, - "search-space": [ - { - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - "prefill": { - "num-worker": 1, - "tp": 8, - "ep": 1, - "dp-attn": False, - }, - "decode": { - "num-worker": 1, - "tp": 8, - "ep": 1, - "dp-attn": False, - }, - } - ], - } - ], + "agentic-coding": [{ + "dram-utilization": 0.80, + "search-space": [{ + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + }], + }], }, }, } @@ -2990,31 +2670,17 @@ def test_multinode_agentic_budget_ignores_decode_topology( "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [ - { - "dram-utilization": 0.80, - "search-space": [ - { - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - # prefill fills the node (8 GPUs); decode uses half. - "prefill": { - "num-worker": 1, - "tp": 8, - "ep": 1, - "dp-attn": False, - }, - "decode": { - "num-worker": 1, - "tp": 4, - "ep": 1, - "dp-attn": False, - }, - } - ], - } - ], + "agentic-coding": [{ + "dram-utilization": 0.80, + "search-space": [{ + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + # prefill fills the node (8 GPUs); decode uses half. + "prefill": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + "decode": {"num-worker": 1, "tp": 4, "ep": 1, "dp-attn": False}, + }], + }], }, }, } @@ -3048,31 +2714,17 @@ def test_multinode_agentic_rejects_node_misaligned_prefill( "disagg": True, "kv-p2p-transfer": "mori", "scenarios": { - "agentic-coding": [ - { - "dram-utilization": 0.80, - "search-space": [ - { - "conc-list": [16], - "kv-offloading": "dram", - "kv-offload-backend": {"name": "hicache"}, - # tp=6 does not divide an 8-GPU node evenly. - "prefill": { - "num-worker": 1, - "tp": 6, - "ep": 1, - "dp-attn": False, - }, - "decode": { - "num-worker": 1, - "tp": 8, - "ep": 1, - "dp-attn": False, - }, - } - ], - } - ], + "agentic-coding": [{ + "dram-utilization": 0.80, + "search-space": [{ + "conc-list": [16], + "kv-offloading": "dram", + "kv-offload-backend": {"name": "hicache"}, + # tp=6 does not divide an 8-GPU node evenly. + "prefill": {"num-worker": 1, "tp": 6, "ep": 1, "dp-attn": False}, + "decode": {"num-worker": 1, "tp": 8, "ep": 1, "dp-attn": False}, + }], + }], }, }, } @@ -3092,7 +2744,6 @@ def test_multinode_agentic_rejects_node_misaligned_prefill( # Test apply_node_type_defaults # ============================================================================= - class TestApplyNodeTypeDefaults: """Tests for apply_node_type_defaults function.""" @@ -3128,56 +2779,49 @@ def test_no_node_attrs_is_noop(self): """When args lacks node type attrs, nothing happens.""" args = argparse.Namespace(command="test-config") apply_node_type_defaults(args) - assert not hasattr(args, "single_node") - assert not hasattr(args, "multi_node") + assert not hasattr(args, 'single_node') + assert not hasattr(args, 'multi_node') # ============================================================================= # Test generate_full_sweep mixed mode # ============================================================================= - class TestGenerateFullSweepMixed: """Tests for generate_full_sweep with both single-node and multi-node configs.""" - def test_both_flags_generates_mixed( - self, sample_mixed_config, sample_runner_config, full_sweep_args_both - ): + def test_both_flags_generates_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_both): """Both flags True should produce both single-node and multinode entries.""" result = generate_full_sweep( - full_sweep_args_both, sample_mixed_config, sample_runner_config + full_sweep_args_both, + sample_mixed_config, + sample_runner_config ) has_single = any("tp" in entry and "prefill" not in entry for entry in result) has_multi = any("prefill" in entry for entry in result) assert has_single, "Expected single-node entries in mixed output" assert has_multi, "Expected multinode entries in mixed output" - def test_single_node_only_from_mixed( - self, sample_mixed_config, sample_runner_config, full_sweep_args_single_node - ): + def test_single_node_only_from_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_single_node): """--single-node should skip multinode entries from mixed config.""" result = generate_full_sweep( - full_sweep_args_single_node, sample_mixed_config, sample_runner_config + full_sweep_args_single_node, + sample_mixed_config, + sample_runner_config ) assert len(result) > 0 - assert all("prefill" not in entry for entry in result), ( - "No multinode entries expected" - ) - assert all("tp" in entry for entry in result), ( - "All entries should have tp field" - ) + assert all("prefill" not in entry for entry in result), "No multinode entries expected" + assert all("tp" in entry for entry in result), "All entries should have tp field" - def test_multi_node_only_from_mixed( - self, sample_mixed_config, sample_runner_config, full_sweep_args_multi_node - ): + def test_multi_node_only_from_mixed(self, sample_mixed_config, sample_runner_config, full_sweep_args_multi_node): """--multi-node should skip single-node entries from mixed config.""" result = generate_full_sweep( - full_sweep_args_multi_node, sample_mixed_config, sample_runner_config + full_sweep_args_multi_node, + sample_mixed_config, + sample_runner_config ) assert len(result) > 0 - assert all("prefill" in entry for entry in result), ( - "All entries should be multinode" - ) + assert all("prefill" in entry for entry in result), "All entries should be multinode" def test_node_type_filters_apply_to_agentic_configs( self, @@ -3196,18 +2840,11 @@ def test_node_type_filters_apply_to_agentic_configs( "runner": "cluster:b300-nv", "multinode": False, "scenarios": { - "agentic-coding": [ - { - "search-space": [ - { - "tp": 4, - "pp": 2, - "kv-offloading": "none", - "conc-list": [16], - }, - ], - } - ], + "agentic-coding": [{ + "search-space": [ + {"tp": 4, "pp": 2, "kv-offloading": "none", "conc-list": [16]}, + ], + }], }, }, "dsv4-agentic-multinode": { @@ -3221,35 +2858,15 @@ def test_node_type_filters_apply_to_agentic_configs( "disagg": True, "kv-p2p-transfer": "nixl", "scenarios": { - "agentic-coding": [ - { - "search-space": [ - { - "conc-list": [16], - "prefill": { - "hardware": "gb200", - "num-worker": 2, - "tp": 4, - "pp": 2, - "dcp-size": 2, - "pcp-size": 2, - "ep": 4, - "dp-attn": False, - }, - "decode": { - "hardware": "h100", - "num-worker": 1, - "tp": 4, - "pp": 2, - "dcp-size": 2, - "pcp-size": 1, - "ep": 1, - "dp-attn": False, - }, - }, - ], - } - ], + "agentic-coding": [{ + "search-space": [ + { + "conc-list": [16], + "prefill": {"hardware": "gb200", "num-worker": 2, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 2, "ep": 4, "dp-attn": False}, + "decode": {"hardware": "h100", "num-worker": 1, "tp": 4, "pp": 2, "dcp-size": 2, "pcp-size": 1, "ep": 1, "dp-attn": False}, + }, + ], + }], }, }, } @@ -3306,14 +2923,17 @@ def test_qwen_b300_fp4_fp8_memory_tier_matrix_is_balanced(self): assert len(result) == 8 assert { - (row["precision"], row["kv-offloading"], row["conc"]) for row in result + (row["precision"], row["kv-offloading"], row["conc"]) + for row in result } == { (precision, offload, conc) for precision in ("fp4", "fp8") for offload in ("none", "dram") for conc in (16, 32) } - assert {row["image"] for row in result} == {"lmsysorg/sglang:v0.5.16-cu130"} + assert {row["image"] for row in result} == { + "lmsysorg/sglang:v0.5.16-cu130" + } assert all(row["runner"] == "cluster:b300-nv" for row in result) assert all(row["tp"] == 2 and row["ep"] == 2 for row in result) assert all(row["spec-decoding"] == "mtp" for row in result) @@ -3374,7 +2994,6 @@ def test_rejects_missing_ambiguous_or_duplicate_names( # Test expand_config_keys # ============================================================================= - class TestExpandConfigKeys: """Tests for expand_config_keys glob/wildcard matching.""" @@ -3428,7 +3047,9 @@ def test_missing_exact_key_raises(self): def test_mixed_exact_and_glob(self): """Mix of exact keys and glob patterns should work.""" - result = expand_config_keys(["dsr1-fp8-h200-trt", "gptoss*"], self.AVAILABLE) + result = expand_config_keys( + ["dsr1-fp8-h200-trt", "gptoss*"], self.AVAILABLE + ) assert result == [ "dsr1-fp8-h200-trt", "gptoss-fp4-b200-vllm", @@ -3450,15 +3071,14 @@ def test_overlapping_patterns_deduplicate(self): # Tests for e2e-tests.yml workflow config splitting # ============================================================================= - def _split_e2e_configs(data): """Replicate the splitting logic from e2e-tests.yml get-jobs step. Returns (SINGLE, MULTI, EVALS) lists matching the workflow filters. """ - single = [x for x in data if "prefill" not in x and not x.get("eval-only", False)] - multi = [x for x in data if "prefill" in x and not x.get("eval-only", False)] - evals = [x for x in data if "prefill" not in x and x.get("run-eval", False)] + single = [x for x in data if 'prefill' not in x and not x.get('eval-only', False)] + multi = [x for x in data if 'prefill' in x and not x.get('eval-only', False)] + evals = [x for x in data if 'prefill' not in x and x.get('run-eval', False)] return single, multi, evals @@ -3472,66 +3092,38 @@ def mixed_entries(self): """Simulates default mode output: single-node (some eval-marked), plus multi-node entries.""" return [ - { - "exp-name": "a", - "isl": 1024, - "osl": 1024, - "conc": 64, - "tp": 2, - "run-eval": False, - }, - { - "exp-name": "b", - "isl": 1024, - "osl": 1024, - "conc": 128, - "tp": 2, - "run-eval": False, - }, - { - "exp-name": "c", - "isl": 8192, - "osl": 1024, - "conc": 256, - "tp": 2, - "run-eval": True, - }, - { - "exp-name": "d", - "isl": 8192, - "osl": 1024, - "conc": 512, - "tp": 2, - "run-eval": True, - }, - {"exp-name": "e", "conc": 64, "prefill": {"tp": 2, "num-worker": 1}}, + {'exp-name': 'a', 'isl': 1024, 'osl': 1024, 'conc': 64, 'tp': 2, 'run-eval': False}, + {'exp-name': 'b', 'isl': 1024, 'osl': 1024, 'conc': 128, 'tp': 2, 'run-eval': False}, + {'exp-name': 'c', 'isl': 8192, 'osl': 1024, 'conc': 256, 'tp': 2, 'run-eval': True}, + {'exp-name': 'd', 'isl': 8192, 'osl': 1024, 'conc': 512, 'tp': 2, 'run-eval': True}, + {'exp-name': 'e', 'conc': 64, 'prefill': {'tp': 2, 'num-worker': 1}}, ] def test_default_mode_benchmarks_all_single_node(self, mixed_entries): """Default: all single-node entries (including eval-marked) are benchmarked.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(single) == 4 - assert all("prefill" not in x for x in single) + assert all('prefill' not in x for x in single) def test_default_mode_evals_only_eval_marked(self, mixed_entries): """Default: only eval-marked entries go to EVALS.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(evals) == 2 - assert all(x["run-eval"] for x in evals) + assert all(x['run-eval'] for x in evals) def test_default_mode_eval_marked_in_both(self, mixed_entries): """Default: eval-marked entries appear in BOTH single and evals.""" single, multi, evals = _split_e2e_configs(mixed_entries) - eval_names = {x["exp-name"] for x in evals} - single_names = {x["exp-name"] for x in single} + eval_names = {x['exp-name'] for x in evals} + single_names = {x['exp-name'] for x in single} assert eval_names.issubset(single_names) def test_no_evals_all_benchmarked(self): """--no-evals: mark_eval_entries is skipped, no run-eval=True entries.""" data = [ - {"exp-name": "a", "conc": 64, "tp": 2, "run-eval": False}, - {"exp-name": "b", "conc": 128, "tp": 2, "run-eval": False}, - {"exp-name": "c", "conc": 256, "tp": 2, "run-eval": False}, + {'exp-name': 'a', 'conc': 64, 'tp': 2, 'run-eval': False}, + {'exp-name': 'b', 'conc': 128, 'tp': 2, 'run-eval': False}, + {'exp-name': 'c', 'conc': 256, 'tp': 2, 'run-eval': False}, ] single, multi, evals = _split_e2e_configs(data) assert len(single) == 3 @@ -3540,20 +3132,8 @@ def test_no_evals_all_benchmarked(self): def test_evals_only_no_benchmarks(self): """--evals-only: entries have eval-only flag, SINGLE must be empty.""" data = [ - { - "exp-name": "c", - "conc": 256, - "tp": 2, - "run-eval": True, - "eval-only": True, - }, - { - "exp-name": "d", - "conc": 512, - "tp": 2, - "run-eval": True, - "eval-only": True, - }, + {'exp-name': 'c', 'conc': 256, 'tp': 2, 'run-eval': True, 'eval-only': True}, + {'exp-name': 'd', 'conc': 512, 'tp': 2, 'run-eval': True, 'eval-only': True}, ] single, multi, evals = _split_e2e_configs(data) assert len(single) == 0, "evals-only should not trigger benchmarks" @@ -3561,22 +3141,10 @@ def test_evals_only_no_benchmarks(self): def test_all_evals_routes_every_fixed_sequence_entry_to_evals(self): data = [ - { - "exp-name": "a", - "isl": 1024, - "conc": 4, - "tp": 2, - "run-eval": True, - "eval-only": True, - }, - { - "exp-name": "b", - "isl": 8192, - "conc": 8, - "tp": 2, - "run-eval": True, - "eval-only": True, - }, + {'exp-name': 'a', 'isl': 1024, 'conc': 4, 'tp': 2, + 'run-eval': True, 'eval-only': True}, + {'exp-name': 'b', 'isl': 8192, 'conc': 8, 'tp': 2, + 'run-eval': True, 'eval-only': True}, ] single, multi, evals = _split_e2e_configs(data) @@ -3595,19 +3163,17 @@ def test_all_eval_marked_without_eval_only_flag_still_benchmarked(self): 8k1k with single conc). Without eval-only flag, SINGLE must still include them for benchmarking.""" data = [ - {"exp-name": "a", "conc": 64, "tp": 2, "run-eval": True}, - {"exp-name": "b", "conc": 64, "tp": 4, "run-eval": True}, + {'exp-name': 'a', 'conc': 64, 'tp': 2, 'run-eval': True}, + {'exp-name': 'b', 'conc': 64, 'tp': 4, 'run-eval': True}, ] single, multi, evals = _split_e2e_configs(data) - assert len(single) == 2, ( - "all-eval-marked entries must still be benchmarked in default mode" - ) + assert len(single) == 2, "all-eval-marked entries must still be benchmarked in default mode" assert len(evals) == 2 def test_prefill_entries_never_in_single_or_evals(self, mixed_entries): """Prefill (multi-node) entries only appear in MULTI.""" single, multi, evals = _split_e2e_configs(mixed_entries) assert len(multi) == 1 - assert all("prefill" in x for x in multi) - assert all("prefill" not in x for x in single) - assert all("prefill" not in x for x in evals) + assert all('prefill' in x for x in multi) + assert all('prefill' not in x for x in single) + assert all('prefill' not in x for x in evals) From 69e9e1d0574afe2693ff893c1e7b2a7bf0b4df5c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:35:00 -0500 Subject: [PATCH 66/99] fix: harden cached image and recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:强化缓存镜像处理并修正配方配置。 --- .../agg-gb200-dep16-throughput-agentic.yaml | 1 - ...hroughput-vllm-simple-offload-agentic.yaml | 1 - .../agg-gb200-tep16-balanced-agentic.yaml | 1 - .../agg-gb200-tp16-latency-agentic.yaml | 1 - runners/launch_h100-dgxc-slurm.sh | 23 ++++++++++--------- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index 87efbb386c..84f8c0929e 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -135,7 +135,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index df90355c79..b945c7cc6c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -138,7 +138,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index 806f571bea..56dd8f7c1a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -127,7 +127,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 4bd2fb36d9..1ee6bc595e 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -124,7 +124,6 @@ srun_options: benchmark: type: custom - aiperf_server_metrics: true command: bash /infmax-workspace/benchmarks/multi_node/agentic_srt.sh env: INFMAX_CONTAINER_WORKSPACE: "/infmax-workspace" diff --git a/runners/launch_h100-dgxc-slurm.sh b/runners/launch_h100-dgxc-slurm.sh index cf619ab747..20599b8bfb 100644 --- a/runners/launch_h100-dgxc-slurm.sh +++ b/runners/launch_h100-dgxc-slurm.sh @@ -304,21 +304,22 @@ else fi trap 'rc=$?; scancel "$JOB_ID" 2>/dev/null || true; exit "$rc"' EXIT - # flock-serialize the enroot import so concurrent sweep jobs on the same - # shared NFS path don't race each other into 'File already exists' (race - # observed on PR #1509: 13/30 jobs failed, all on the dgxc-slurm runners - # hitting the same /mnt/nfs/lustre/containers/.sqsh path). Matches - # the canonical pattern already used in launch_h100-cw.sh + the mi3xx - # launchers. The skip-if-valid check avoids re-downloading when the file - # was successfully created by an earlier job. + # Check the shared cache before opening its lock. A valid squash file is + # immutable, so readers do not need to touch a lock owned by another user. srun --jobid=$JOB_ID bash -c " - exec 9>\"$LOCK_FILE\" - flock -w 600 9 || { echo 'Failed to acquire lock for $SQUASH_FILE'; exit 1; } if unsquashfs -l \"$SQUASH_FILE\" > /dev/null 2>&1; then echo 'Squash file already exists and is valid, skipping import' else - rm -f \"$SQUASH_FILE\" - enroot import -o \"$SQUASH_FILE\" docker://$IMAGE + if ! { exec 9>\"$LOCK_FILE\"; } 2>/dev/null; then + exec 9<\"$LOCK_FILE\" || { echo 'Failed to open lock for $SQUASH_FILE'; exit 1; } + fi + flock -w 600 9 || { echo 'Failed to acquire lock for $SQUASH_FILE'; exit 1; } + if unsquashfs -l \"$SQUASH_FILE\" > /dev/null 2>&1; then + echo 'Squash file was imported by another job' + else + rm -f \"$SQUASH_FILE\" + enroot import -o \"$SQUASH_FILE\" docker://$IMAGE + fi fi " From c8137271c524e0ee64d30205fdb53d833adfa46d Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:50:28 -0500 Subject: [PATCH 67/99] fix: restore GB200 draft model source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:恢复 GB200 配方中的投机解码草稿模型来源。 --- .../kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml | 2 +- .../agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml | 2 +- .../vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml | 2 +- .../vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml index 84f8c0929e..f1fde0a104 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-agentic.yaml @@ -114,7 +114,7 @@ backend: # runtime workspace for FlashInfer's MXFP4 MoE kernel. max-num-seqs: 64 max-num-batched-tokens: 16384 - speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' + speculative-config: '{"method":"dspark","model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' compilation-config: '{"cudagraph_mode":"PIECEWISE","cudagraph_capture_sizes":[3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72,75,78,81,84,87,90,93,96,99,102,105,108,111,114,117,120,123,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,177,180,183,186,189,192]}' block-size: 64 language-model-only: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml index b945c7cc6c..b748d10f21 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-dep16-throughput-vllm-simple-offload-agentic.yaml @@ -116,7 +116,7 @@ backend: # sizes are exact hits, while odd loads pad by at most one sequence. max-num-seqs: 96 max-num-batched-tokens: 16384 - speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' + speculative-config: '{"method":"dspark","model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' compilation-config: '{"cudagraph_mode":"PIECEWISE","cudagraph_capture_sizes":[6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102,108,114,120,126,132,138,144,150,156,162,168,174,180,186,192,198,204,210,216,222,228,234,240,246,252,258,264,270,276,282,288]}' block-size: 64 language-model-only: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml index 56dd8f7c1a..be797ce972 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tep16-balanced-agentic.yaml @@ -106,7 +106,7 @@ backend: gpu-memory-utilization: 0.92 max-num-seqs: 32 max-num-batched-tokens: 8192 - speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' + speculative-config: '{"method":"dspark","model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72,75,78,81,84,87,90,93,96],"pass_config":{"fuse_allreduce_rms":false}}' block-size: 64 language-model-only: true diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml index 1ee6bc595e..221ab1693e 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic/agg-gb200-tp16-latency-agentic.yaml @@ -103,7 +103,7 @@ backend: gpu-memory-utilization: 0.92 max-num-seqs: 8 max-num-batched-tokens: 8192 - speculative-config: '{"method":"dspark","model":"/tmp/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' + speculative-config: '{"method":"dspark","model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":2,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"probabilistic","rejection_sample_method":"synthetic","synthetic_acceptance_length":2.51}' compilation-config: '{"cudagraph_capture_sizes":[3,6,9,12,15,18,21,24],"pass_config":{"fuse_allreduce_rms":false}}' block-size: 64 language-model-only: true From aac233a15369f5cf1fcecedf339eaf59aae27938 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:24:38 -0500 Subject: [PATCH 68/99] fix: gate tool evaluations on readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在开始工具调用评估前等待 OpenAI 模型端点就绪,并通过完整适配器准备固定版本的 MiniMax 上游源码。 --- benchmarks/benchmark_lib.sh | 8 +-- benchmarks/multi_node/agentic_srt.sh | 25 +++++++ utils/evals/test_run_eval_dispatch.py | 98 +++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index bf3b6bc641..32e1093562 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1777,7 +1777,7 @@ _run_minimax_m3_smoke_eval() { integration_error="MiniMax Provider Verifier Python runtime preparation failed with exit code ${setup_rc}" } if [ "$setup_rc" -eq 0 ]; then - runtime_dir=$(_prepare_minimax_m3_full_runtime "$adapter_path") || { + runtime_dir=$(_prepare_minimax_m3_full_runtime) || { setup_rc=$? integration_error="MiniMax Provider Verifier pinned runtime preparation failed with exit code ${setup_rc}" } @@ -1834,10 +1834,10 @@ _install_minimax_m3_full_deps() { } _prepare_minimax_m3_full_runtime() { - local adapter_path="$1" + local source_adapter_path="${INFERENCEX_REPO_ROOT}/utils/evals/minimax_m3_full_eval.py" local runtime_dir prepare_rc=0 runtime_dir="$(mktemp -d /tmp/minimax-m3-full-runtime-XXXXXX)" || return $? - "${VENDOR_VERIFIER_PYTHON:-python3}" "$adapter_path" prepare-source \ + "${VENDOR_VERIFIER_PYTHON:-python3}" "$source_adapter_path" prepare-source \ --source-dir "${runtime_dir}/source" >&2 || prepare_rc=$? if [ "$prepare_rc" -eq 0 ]; then _install_minimax_m3_full_deps "${runtime_dir}/deps" >&2 || prepare_rc=$? @@ -1903,7 +1903,7 @@ _run_minimax_m3_full_eval() { integration_error="MiniMax M3 full Python runtime preparation failed with exit code ${setup_rc}" } if [ "$setup_rc" -eq 0 ]; then - runtime_dir=$(_prepare_minimax_m3_full_runtime "$adapter_path") || { + runtime_dir=$(_prepare_minimax_m3_full_runtime) || { setup_rc=$? integration_error="MiniMax M3 full pinned runtime preparation failed with exit code ${setup_rc}" } diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index 0ea94e0bde..05e7397e3a 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -39,8 +39,33 @@ for concurrency in "${CONCURRENCIES[@]}"; do fi done +wait_for_openai_endpoint_ready() { + local timeout_seconds="${AIPERF_ENDPOINT_READY_TIMEOUT_SECONDS:-1800}" + local poll_seconds=5 + local start_seconds=$SECONDS + local next_report=0 + local elapsed percent + local models_url="http://localhost:${PORT}/v1/models" + + while ! curl -fsS --max-time 10 "$models_url" >/dev/null 2>&1; do + elapsed=$((SECONDS - start_seconds)) + if [ "$elapsed" -ge "$timeout_seconds" ]; then + echo "ERROR: OpenAI endpoint did not become ready within ${timeout_seconds}s: $models_url" >&2 + return 1 + fi + if [ "$elapsed" -ge "$next_report" ]; then + percent=$((elapsed * 100 / timeout_seconds)) + echo "Waiting for OpenAI endpoint: ${elapsed}/${timeout_seconds}s (${percent}%)" + next_report=$((next_report + 60)) + fi + sleep "$poll_seconds" + done + echo "OpenAI endpoint ready: $models_url" +} + resolve_trace_source install_agentic_deps +wait_for_openai_endpoint_ready wait_for_agentic_servers_idle() { local timeout_seconds="${AIPERF_DRAIN_TIMEOUT_SECONDS:-1800}" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index ac3d0a2e7a..51e2c4dc36 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -18,6 +18,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] BENCHMARK_LIB = REPO_ROOT / "benchmarks" / "benchmark_lib.sh" +MULTINODE_AGENTIC_SCRIPT = REPO_ROOT / "benchmarks/multi_node/agentic_srt.sh" SINGLE_NODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-tmpl.yml" MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" @@ -598,6 +599,45 @@ def test_minimax_full_dependency_install_matches_pinned_upstream_requirements( assert "--break-system-packages" not in result.stdout +def test_minimax_runtime_prepares_source_with_full_adapter(tmp_path: Path) -> None: + runtime_dir = tmp_path / "runtime" + calls_path = tmp_path / "calls" + script = r""" +source "$BENCHMARK_LIB" +mktemp() { + mkdir -p "$RUNTIME_DIR" + printf '%s\n' "$RUNTIME_DIR" +} +selected_python() { + printf 'PYTHON_ARG=<%s>\n' "$@" >> "$CALLS_PATH" + mkdir -p "$RUNTIME_DIR/source" +} +_install_minimax_m3_full_deps() { mkdir -p "$1"; } +VENDOR_VERIFIER_PYTHON=selected_python +prepared_runtime=$(_prepare_minimax_m3_full_runtime) +printf 'RUNTIME=<%s>\n' "$prepared_runtime" +""" + result = subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "RUNTIME_DIR": str(runtime_dir), + "CALLS_PATH": str(calls_path), + }, + text=True, + capture_output=True, + check=True, + ) + calls = calls_path.read_text() + + assert f"PYTHON_ARG=<{REPO_ROOT / 'utils/evals/minimax_m3_full_eval.py'}>" in calls + assert "PYTHON_ARG=" in calls + assert f"PYTHON_ARG=<{runtime_dir / 'source'}>" in calls + assert "minimax_provider_eval.py" not in calls + assert f"RUNTIME=<{runtime_dir}>" in result.stdout + + def test_minimax_vendor_runner_uses_fixed_adapter_contract(tmp_path: Path) -> None: results_dir = tmp_path / "results" runtime_dir = tmp_path / "runtime" @@ -2463,6 +2503,64 @@ def test_qwen_sglang_launchers_expose_structured_tool_calls() -> None: assert "--tool-call-parser qwen3_coder" in command +def test_multinode_agentic_waits_for_openai_endpoint_before_requests( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + bin_dir = tmp_path / "bin" + events_path = tmp_path / "events" + (workspace / "benchmarks").mkdir(parents=True) + bin_dir.mkdir() + (workspace / "benchmarks/benchmark_lib.sh").write_text( + """ +PORT=8765 +check_env_vars() { :; } +resolve_trace_source() { echo resolve >> "$EVENTS"; } +install_agentic_deps() { echo deps >> "$EVENTS"; } +build_replay_cmd() { echo build >> "$EVENTS"; } +run_agentic_replay_and_write_outputs() { echo replay >> "$EVENTS"; } +""", + encoding="utf-8", + ) + curl = bin_dir / "curl" + curl.write_text( + """#!/usr/bin/env bash +printf 'curl %s\n' "$*" >> "$EVENTS" +""", + encoding="utf-8", + ) + curl.chmod(curl.stat().st_mode | stat.S_IXUSR) + + subprocess.run( + ["bash", str(MULTINODE_AGENTIC_SCRIPT)], + env={ + **os.environ, + "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", + "INFMAX_CONTAINER_WORKSPACE": str(workspace), + "EVENTS": str(events_path), + "MODEL": "test-model", + "MODEL_PREFIX": "test-prefix", + "FRAMEWORK": "dynamo-vllm", + "PRECISION": "fp4", + "CONC": "1", + "RESULT_FILENAME": "result", + "RESULT_DIR": str(tmp_path / "results"), + "DURATION": "1", + }, + text=True, + capture_output=True, + check=True, + ) + + assert events_path.read_text().splitlines() == [ + "resolve", + "deps", + "curl -fsS --max-time 10 http://localhost:8765/v1/models", + "build", + "replay", + ] + + def test_agentic_eval_workflow_forwards_runner_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) forwarded = workflow["jobs"]["test-sweep-agentic-evals"]["with"] From b74fe204e22a3bc8d8bf4b4a1fa0e4fe118c375d Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:29:23 -0500 Subject: [PATCH 69/99] fix: require registered model before evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在多节点工具调用评估开始前,要求 OpenAI 模型列表包含目标服务模型,避免前端已启动但聊天路由尚未注册的竞态。 --- benchmarks/multi_node/agentic_srt.sh | 17 +++++++++++++---- utils/evals/test_run_eval_dispatch.py | 2 ++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index 05e7397e3a..78f7780781 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -45,22 +45,31 @@ wait_for_openai_endpoint_ready() { local start_seconds=$SECONDS local next_report=0 local elapsed percent + local served_model="${SERVED_MODEL_NAME:-$MODEL}" local models_url="http://localhost:${PORT}/v1/models" + while ! curl -fsS --max-time 10 "$models_url" 2>/dev/null \ + | "$AIPERF_PYTHON" -c ' +import json +import sys - while ! curl -fsS --max-time 10 "$models_url" >/dev/null 2>&1; do +expected = sys.argv[1] +payload = json.load(sys.stdin) +models = payload.get("data", []) +raise SystemExit(0 if any(model.get("id") == expected for model in models) else 1) +' "$served_model" >/dev/null 2>&1; do elapsed=$((SECONDS - start_seconds)) if [ "$elapsed" -ge "$timeout_seconds" ]; then - echo "ERROR: OpenAI endpoint did not become ready within ${timeout_seconds}s: $models_url" >&2 + echo "ERROR: model '$served_model' did not become ready within ${timeout_seconds}s: $models_url" >&2 return 1 fi if [ "$elapsed" -ge "$next_report" ]; then percent=$((elapsed * 100 / timeout_seconds)) - echo "Waiting for OpenAI endpoint: ${elapsed}/${timeout_seconds}s (${percent}%)" + echo "Waiting for model '$served_model': ${elapsed}/${timeout_seconds}s (${percent}%)" next_report=$((next_report + 60)) fi sleep "$poll_seconds" done - echo "OpenAI endpoint ready: $models_url" + echo "OpenAI model ready: $served_model at $models_url" } resolve_trace_source diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 51e2c4dc36..d3054b37e5 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2516,6 +2516,7 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( PORT=8765 check_env_vars() { :; } resolve_trace_source() { echo resolve >> "$EVENTS"; } +AIPERF_PYTHON=python3 install_agentic_deps() { echo deps >> "$EVENTS"; } build_replay_cmd() { echo build >> "$EVENTS"; } run_agentic_replay_and_write_outputs() { echo replay >> "$EVENTS"; } @@ -2526,6 +2527,7 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( curl.write_text( """#!/usr/bin/env bash printf 'curl %s\n' "$*" >> "$EVENTS" +printf '{"data":[{"id":"test-model"}]}\n' """, encoding="utf-8", ) From 533a027c6a957ca66f5a73fb280c3a137beaad48 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:57:34 -0500 Subject: [PATCH 70/99] fix: wait for active chat route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在多节点工具调用评估前使用无推理请求确认聊天路由已激活,避免模型已列出但 Dynamo 路由仍返回 404 的竞态。 --- benchmarks/multi_node/agentic_srt.sh | 31 +++++++++++++++++++++------ utils/evals/test_run_eval_dispatch.py | 17 ++++++++------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index 78f7780781..6ea8ece56f 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -44,11 +44,15 @@ wait_for_openai_endpoint_ready() { local poll_seconds=5 local start_seconds=$SECONDS local next_report=0 - local elapsed percent + local elapsed percent chat_status local served_model="${SERVED_MODEL_NAME:-$MODEL}" local models_url="http://localhost:${PORT}/v1/models" - while ! curl -fsS --max-time 10 "$models_url" 2>/dev/null \ - | "$AIPERF_PYTHON" -c ' + local chat_url="http://localhost:${PORT}/v1/chat/completions" + + while true; do + local model_ready=false + if curl -fsS --max-time 10 "$models_url" 2>/dev/null \ + | "$AIPERF_PYTHON" -c ' import json import sys @@ -56,20 +60,33 @@ expected = sys.argv[1] payload = json.load(sys.stdin) models = payload.get("data", []) raise SystemExit(0 if any(model.get("id") == expected for model in models) else 1) -' "$served_model" >/dev/null 2>&1; do +' "$served_model" >/dev/null 2>&1; then + model_ready=true + fi + + chat_status="" + if [ "$model_ready" = true ]; then + chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ + -H 'Content-Type: application/json' --data '{}' "$chat_url" 2>/dev/null)" \ + || true + case "$chat_status" in + 400|401|403|422) break ;; + esac + fi + elapsed=$((SECONDS - start_seconds)) if [ "$elapsed" -ge "$timeout_seconds" ]; then - echo "ERROR: model '$served_model' did not become ready within ${timeout_seconds}s: $models_url" >&2 + echo "ERROR: chat endpoint for model '$served_model' did not become ready within ${timeout_seconds}s: $chat_url" >&2 return 1 fi if [ "$elapsed" -ge "$next_report" ]; then percent=$((elapsed * 100 / timeout_seconds)) - echo "Waiting for model '$served_model': ${elapsed}/${timeout_seconds}s (${percent}%)" + echo "Waiting for chat endpoint for model '$served_model': ${elapsed}/${timeout_seconds}s (${percent}%)" next_report=$((next_report + 60)) fi sleep "$poll_seconds" done - echo "OpenAI model ready: $served_model at $models_url" + echo "OpenAI chat endpoint ready for model '$served_model': $chat_url" } resolve_trace_source diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index d3054b37e5..e5318583e7 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2527,7 +2527,10 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( curl.write_text( """#!/usr/bin/env bash printf 'curl %s\n' "$*" >> "$EVENTS" -printf '{"data":[{"id":"test-model"}]}\n' +case "$*" in + */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; + */v1/chat/completions*) printf '422' ;; +esac """, encoding="utf-8", ) @@ -2554,13 +2557,11 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( check=True, ) - assert events_path.read_text().splitlines() == [ - "resolve", - "deps", - "curl -fsS --max-time 10 http://localhost:8765/v1/models", - "build", - "replay", - ] + events = events_path.read_text().splitlines() + assert events[:2] == ["resolve", "deps"] + assert events[2].endswith("http://localhost:8765/v1/models") + assert events[3].endswith("http://localhost:8765/v1/chat/completions") + assert events[-2:] == ["build", "replay"] def test_agentic_eval_workflow_forwards_runner_contract() -> None: From 8ef810724c4eeffaf6aa5172ead83c2da54a3bba Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:30:16 -0500 Subject: [PATCH 71/99] fix: gate eval dispatcher on route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将聊天路由就绪检查放入多节点实际调用的统一评估入口,并复用于 AgentX 回放,确保工具调用评估不会早于 Dynamo 路由注册。 --- benchmarks/benchmark_lib.sh | 83 +++++++++++++++++++++++++++ benchmarks/multi_node/agentic_srt.sh | 51 +--------------- utils/evals/test_run_eval_dispatch.py | 71 ++++++++++++++++------- 3 files changed, 135 insertions(+), 70 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 32e1093562..37cffa9eac 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2772,6 +2772,81 @@ run_swebench_eval() { fi } +_wait_for_openai_chat_route() { + local port="${PORT:-8888}" + local timeout_seconds="${EVAL_ENDPOINT_READY_TIMEOUT_SECONDS:-1800}" + local poll_seconds=5 + local start_seconds=$SECONDS + local next_report=0 + local elapsed percent chat_status + local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" + local models_url chat_url + + while [[ $# -gt 0 ]]; do + case "$1" in + --port) + if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then + echo "ERROR: --port requires a value" >&2 + return 2 + fi + port="$2" + shift 2 + ;; + *) shift ;; + esac + done + if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then + echo "ERROR: EVAL_ENDPOINT_READY_TIMEOUT_SECONDS must be a positive integer" >&2 + return 2 + fi + if [ -z "$served_model" ]; then + echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 + return 2 + fi + models_url="http://localhost:${port}/v1/models" + chat_url="http://localhost:${port}/v1/chat/completions" + + while true; do + local model_ready=false + if curl -fsS --max-time 10 "$models_url" 2>/dev/null \ + | python3 -c ' +import json +import sys + +expected = sys.argv[1] +payload = json.load(sys.stdin) +models = payload.get("data", []) +raise SystemExit(0 if any(model.get("id") == expected for model in models) else 1) +' "$served_model" >/dev/null 2>&1; then + model_ready=true + fi + + chat_status="" + if [ "$model_ready" = true ]; then + chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ + -H 'Content-Type: application/json' --data '{}' "$chat_url" 2>/dev/null)" \ + || true + case "$chat_status" in + 400|401|403|422) break ;; + esac + fi + + elapsed=$((SECONDS - start_seconds)) + if [ "$elapsed" -ge "$timeout_seconds" ]; then + echo "ERROR: chat endpoint for model '$served_model' did not become ready within ${timeout_seconds}s: $chat_url" >&2 + return 1 + fi + if [ "$elapsed" -ge "$next_report" ]; then + percent=$((elapsed * 100 / timeout_seconds)) + echo "Waiting for chat endpoint for model '$served_model': ${elapsed}/${timeout_seconds}s (${percent}%)" + next_report=$((next_report + 60)) + fi + sleep "$poll_seconds" + done + echo "OpenAI chat endpoint ready for model '$served_model': $chat_url" +} + + # ------------------------------ # Unified eval entrypoint # ------------------------------ @@ -2835,6 +2910,14 @@ run_eval() { return 2 fi + if [ "${EVAL_ONLY:-false}" = "true" ]; then + case "$framework" in + kimi-vendor|minimax-vendor|bfcl) + _wait_for_openai_chat_route "${forwarded[@]}" || return $? + ;; + esac + fi + # Explicit verifier suites use fixed request budgets and do not consume # EVAL_MAX_MODEL_LEN, so avoid loading model configuration for those paths. if [ "$framework" != "kimi-vendor" ] \ diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index 6ea8ece56f..f88d3f5b71 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -39,59 +39,10 @@ for concurrency in "${CONCURRENCIES[@]}"; do fi done -wait_for_openai_endpoint_ready() { - local timeout_seconds="${AIPERF_ENDPOINT_READY_TIMEOUT_SECONDS:-1800}" - local poll_seconds=5 - local start_seconds=$SECONDS - local next_report=0 - local elapsed percent chat_status - local served_model="${SERVED_MODEL_NAME:-$MODEL}" - local models_url="http://localhost:${PORT}/v1/models" - local chat_url="http://localhost:${PORT}/v1/chat/completions" - - while true; do - local model_ready=false - if curl -fsS --max-time 10 "$models_url" 2>/dev/null \ - | "$AIPERF_PYTHON" -c ' -import json -import sys - -expected = sys.argv[1] -payload = json.load(sys.stdin) -models = payload.get("data", []) -raise SystemExit(0 if any(model.get("id") == expected for model in models) else 1) -' "$served_model" >/dev/null 2>&1; then - model_ready=true - fi - - chat_status="" - if [ "$model_ready" = true ]; then - chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ - -H 'Content-Type: application/json' --data '{}' "$chat_url" 2>/dev/null)" \ - || true - case "$chat_status" in - 400|401|403|422) break ;; - esac - fi - - elapsed=$((SECONDS - start_seconds)) - if [ "$elapsed" -ge "$timeout_seconds" ]; then - echo "ERROR: chat endpoint for model '$served_model' did not become ready within ${timeout_seconds}s: $chat_url" >&2 - return 1 - fi - if [ "$elapsed" -ge "$next_report" ]; then - percent=$((elapsed * 100 / timeout_seconds)) - echo "Waiting for chat endpoint for model '$served_model': ${elapsed}/${timeout_seconds}s (${percent}%)" - next_report=$((next_report + 60)) - fi - sleep "$poll_seconds" - done - echo "OpenAI chat endpoint ready for model '$served_model': $chat_url" -} resolve_trace_source install_agentic_deps -wait_for_openai_endpoint_ready +_wait_for_openai_chat_route --port "$PORT" wait_for_agentic_servers_idle() { local timeout_seconds="${AIPERF_DRAIN_TIMEOUT_SECONDS:-1800}" diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index e5318583e7..eb0e5523de 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -29,6 +29,7 @@ _SCRIPT = r""" source "$BENCHMARK_LIB" +_wait_for_openai_chat_route() { echo "READY=$*"; } run_lm_eval() { echo "DISPATCH=lm-eval"; } run_swebench_eval() { echo "DISPATCH=swebench"; } run_kimi_vendor_eval() { echo "DISPATCH=kimi-vendor"; } @@ -263,6 +264,7 @@ def test_run_eval_scopes_runner_selected_suite_to_one_call() -> None: def test_kimi_default_suite_reaches_eval_only_metadata() -> None: script = r""" source "$BENCHMARK_LIB" +_wait_for_openai_chat_route() { :; } run_kimi_vendor_eval() { echo "DISPATCH=$EVAL_SUITE"; } append_lm_eval_summary() { echo "METADATA=$EVAL_COMPLETED_SUITE"; } export EVAL_FRAMEWORK=kimi-vendor @@ -288,6 +290,7 @@ def test_kimi_default_suite_reaches_eval_only_metadata() -> None: def test_agentic_eval_propagates_artifact_staging_failure() -> None: script = r""" source "$BENCHMARK_LIB" +_wait_for_openai_chat_route() { :; } run_kimi_vendor_eval() { :; } append_lm_eval_summary() { return 73; } export EVAL_FRAMEWORK=kimi-vendor @@ -2503,14 +2506,52 @@ def test_qwen_sglang_launchers_expose_structured_tool_calls() -> None: assert "--tool-call-parser qwen3_coder" in command +def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> None: + bin_dir = tmp_path / "bin" + events_path = tmp_path / "events" + bin_dir.mkdir() + curl = bin_dir / "curl" + curl.write_text( + """#!/usr/bin/env bash +printf 'curl %s\n' "$*" >> "$EVENTS" +case "$*" in + */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; + */v1/chat/completions*) printf '422' ;; +esac +""", + encoding="utf-8", + ) + curl.chmod(curl.stat().st_mode | stat.S_IXUSR) + script = r""" +source "$BENCHMARK_LIB" +MODEL=test-model +_wait_for_openai_chat_route --port 8765 +""" + + subprocess.run( + ["bash", "-c", script], + env={ + **os.environ, + "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "EVENTS": str(events_path), + }, + text=True, + capture_output=True, + check=True, + ) + + events = events_path.read_text().splitlines() + assert events[0].endswith("http://localhost:8765/v1/models") + assert events[1].endswith("http://localhost:8765/v1/chat/completions") + + def test_multinode_agentic_waits_for_openai_endpoint_before_requests( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" - bin_dir = tmp_path / "bin" events_path = tmp_path / "events" (workspace / "benchmarks").mkdir(parents=True) - bin_dir.mkdir() (workspace / "benchmarks/benchmark_lib.sh").write_text( """ PORT=8765 @@ -2518,29 +2559,17 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( resolve_trace_source() { echo resolve >> "$EVENTS"; } AIPERF_PYTHON=python3 install_agentic_deps() { echo deps >> "$EVENTS"; } +_wait_for_openai_chat_route() { echo "ready $*" >> "$EVENTS"; } build_replay_cmd() { echo build >> "$EVENTS"; } run_agentic_replay_and_write_outputs() { echo replay >> "$EVENTS"; } """, encoding="utf-8", ) - curl = bin_dir / "curl" - curl.write_text( - """#!/usr/bin/env bash -printf 'curl %s\n' "$*" >> "$EVENTS" -case "$*" in - */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; - */v1/chat/completions*) printf '422' ;; -esac -""", - encoding="utf-8", - ) - curl.chmod(curl.stat().st_mode | stat.S_IXUSR) subprocess.run( ["bash", str(MULTINODE_AGENTIC_SCRIPT)], env={ **os.environ, - "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "INFMAX_CONTAINER_WORKSPACE": str(workspace), "EVENTS": str(events_path), "MODEL": "test-model", @@ -2557,11 +2586,13 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( check=True, ) - events = events_path.read_text().splitlines() - assert events[:2] == ["resolve", "deps"] - assert events[2].endswith("http://localhost:8765/v1/models") - assert events[3].endswith("http://localhost:8765/v1/chat/completions") - assert events[-2:] == ["build", "replay"] + assert events_path.read_text().splitlines() == [ + "resolve", + "deps", + "ready --port 8765", + "build", + "replay", + ] def test_agentic_eval_workflow_forwards_runner_contract() -> None: From 0bfb0fba00fe690aa8851add412ca1514bf0e2c9 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:02:37 -0500 Subject: [PATCH 72/99] fix: preserve selected TRT eval framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留工作流选择的 TRT MiniMax 工具调用评估框架,仅在未提供覆盖值时默认使用 lm-eval。 --- benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh | 2 +- benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh index 686145460b..c83b53aef2 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh @@ -11,7 +11,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EVAL_ONLY diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh index df11cae903..aebc8390d3 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh @@ -11,7 +11,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" -export EVAL_FRAMEWORK="lm-eval" +export EVAL_FRAMEWORK="${EVAL_FRAMEWORK:-lm-eval}" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EVAL_ONLY From b6d4085d07e45b479384fbcd75055b7b239f1676 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:32:02 -0500 Subject: [PATCH 73/99] fix: send model-aware readiness probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:发送包含模型信息的就绪探测请求 --- benchmarks/benchmark_lib.sh | 10 ++++++++-- utils/evals/test_run_eval_dispatch.py | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 37cffa9eac..b203b11918 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2778,7 +2778,7 @@ _wait_for_openai_chat_route() { local poll_seconds=5 local start_seconds=$SECONDS local next_report=0 - local elapsed percent chat_status + local elapsed percent chat_status chat_probe_payload local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" local models_url chat_url @@ -2803,6 +2803,12 @@ _wait_for_openai_chat_route() { echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 return 2 fi + chat_probe_payload="$(python3 -c ' +import json +import sys + +print(json.dumps({"model": sys.argv[1], "messages": [], "max_tokens": 1})) +' "$served_model")" || return $? models_url="http://localhost:${port}/v1/models" chat_url="http://localhost:${port}/v1/chat/completions" @@ -2824,7 +2830,7 @@ raise SystemExit(0 if any(model.get("id") == expected for model in models) else chat_status="" if [ "$model_ready" = true ]; then chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ - -H 'Content-Type: application/json' --data '{}' "$chat_url" 2>/dev/null)" \ + -H 'Content-Type: application/json' --data "$chat_probe_payload" "$chat_url" 2>/dev/null)" \ || true case "$chat_status" in 400|401|403|422) break ;; diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index eb0e5523de..bf3ff9f126 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2544,6 +2544,7 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> events = events_path.read_text().splitlines() assert events[0].endswith("http://localhost:8765/v1/models") assert events[1].endswith("http://localhost:8765/v1/chat/completions") + assert '--data {"model": "test-model", "messages": [], "max_tokens": 1}' in events[1] def test_multinode_agentic_waits_for_openai_endpoint_before_requests( From 529b3434fbcc1c0e7da91369f808bf2c982505cd Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:50:16 -0500 Subject: [PATCH 74/99] fix: probe chat route registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:探测聊天路由注册状态 --- benchmarks/benchmark_lib.sh | 13 +++---------- utils/evals/test_run_eval_dispatch.py | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index b203b11918..ead0030fc1 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2778,7 +2778,7 @@ _wait_for_openai_chat_route() { local poll_seconds=5 local start_seconds=$SECONDS local next_report=0 - local elapsed percent chat_status chat_probe_payload + local elapsed percent chat_status local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" local models_url chat_url @@ -2803,12 +2803,6 @@ _wait_for_openai_chat_route() { echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 return 2 fi - chat_probe_payload="$(python3 -c ' -import json -import sys - -print(json.dumps({"model": sys.argv[1], "messages": [], "max_tokens": 1})) -' "$served_model")" || return $? models_url="http://localhost:${port}/v1/models" chat_url="http://localhost:${port}/v1/chat/completions" @@ -2830,10 +2824,9 @@ raise SystemExit(0 if any(model.get("id") == expected for model in models) else chat_status="" if [ "$model_ready" = true ]; then chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ - -H 'Content-Type: application/json' --data "$chat_probe_payload" "$chat_url" 2>/dev/null)" \ - || true + "$chat_url" 2>/dev/null)" || true case "$chat_status" in - 400|401|403|422) break ;; + 401|403|405) break ;; esac fi diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index bf3ff9f126..0a565b5a55 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2516,7 +2516,7 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> printf 'curl %s\n' "$*" >> "$EVENTS" case "$*" in */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; - */v1/chat/completions*) printf '422' ;; + */v1/chat/completions*) printf '405' ;; esac """, encoding="utf-8", @@ -2544,7 +2544,7 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> events = events_path.read_text().splitlines() assert events[0].endswith("http://localhost:8765/v1/models") assert events[1].endswith("http://localhost:8765/v1/chat/completions") - assert '--data {"model": "test-model", "messages": [], "max_tokens": 1}' in events[1] + assert "--data" not in events[1] def test_multinode_agentic_waits_for_openai_endpoint_before_requests( From bc097ac95a5d408b3b8e64beacc6c57b9d72bba2 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:08:45 -0500 Subject: [PATCH 75/99] fix: stabilize registered models before eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:评估前等待已注册模型稳定 --- benchmarks/benchmark_lib.sh | 14 +++++++++++ utils/evals/test_run_eval_dispatch.py | 34 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index ead0030fc1..a60a818d2e 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2776,7 +2776,9 @@ _wait_for_openai_chat_route() { local port="${PORT:-8888}" local timeout_seconds="${EVAL_ENDPOINT_READY_TIMEOUT_SECONDS:-1800}" local poll_seconds=5 + local stabilization_seconds="${EVAL_MODEL_STABILIZATION_SECONDS:-30}" local start_seconds=$SECONDS + local model_ready_since=-1 local next_report=0 local elapsed percent chat_status local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" @@ -2799,6 +2801,10 @@ _wait_for_openai_chat_route() { echo "ERROR: EVAL_ENDPOINT_READY_TIMEOUT_SECONDS must be a positive integer" >&2 return 2 fi + if ! [[ "$stabilization_seconds" =~ ^[0-9]+$ ]]; then + echo "ERROR: EVAL_MODEL_STABILIZATION_SECONDS must be a non-negative integer" >&2 + return 2 + fi if [ -z "$served_model" ]; then echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 return 2 @@ -2823,11 +2829,19 @@ raise SystemExit(0 if any(model.get("id") == expected for model in models) else chat_status="" if [ "$model_ready" = true ]; then + if [ "$model_ready_since" -lt 0 ]; then + model_ready_since=$SECONDS + fi chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ "$chat_url" 2>/dev/null)" || true case "$chat_status" in 401|403|405) break ;; esac + if [ $((SECONDS - model_ready_since)) -ge "$stabilization_seconds" ]; then + break + fi + else + model_ready_since=-1 fi elapsed=$((SECONDS - start_seconds)) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 0a565b5a55..7c319f7dfc 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2547,6 +2547,40 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> assert "--data" not in events[1] +def test_chat_route_readiness_accepts_stable_registered_model(tmp_path: Path) -> None: + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + curl = bin_dir / "curl" + curl.write_text( + """#!/usr/bin/env bash +case "$*" in + */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; + */v1/chat/completions*) printf '404' ;; +esac +""", + encoding="utf-8", + ) + curl.chmod(curl.stat().st_mode | stat.S_IXUSR) + + subprocess.run( + [ + "bash", + "-c", + 'source "$BENCHMARK_LIB"; MODEL=test-model; ' + "_wait_for_openai_chat_route --port 8765", + ], + env={ + **os.environ, + "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "EVAL_MODEL_STABILIZATION_SECONDS": "0", + }, + text=True, + capture_output=True, + check=True, + ) + + def test_multinode_agentic_waits_for_openai_endpoint_before_requests( tmp_path: Path, ) -> None: From 089e707a8be7776443a9ee49d63ee410e35abc83 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 02:30:35 -0500 Subject: [PATCH 76/99] fix: accept stable OpenAI server readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:接受稳定的 OpenAI 服务就绪状态 --- benchmarks/benchmark_lib.sh | 21 ++++++++++++++------- utils/evals/test_run_eval_dispatch.py | 13 ++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index a60a818d2e..30231c090f 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2778,11 +2778,11 @@ _wait_for_openai_chat_route() { local poll_seconds=5 local stabilization_seconds="${EVAL_MODEL_STABILIZATION_SECONDS:-30}" local start_seconds=$SECONDS - local model_ready_since=-1 + local server_ready_since=-1 local next_report=0 local elapsed percent chat_status local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" - local models_url chat_url + local root_url models_url chat_url while [[ $# -gt 0 ]]; do case "$1" in @@ -2809,11 +2809,16 @@ _wait_for_openai_chat_route() { echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 return 2 fi + root_url="http://localhost:${port}/" models_url="http://localhost:${port}/v1/models" chat_url="http://localhost:${port}/v1/chat/completions" while true; do local model_ready=false + local server_ready=false + if curl -fsS --max-time 10 "$root_url" >/dev/null 2>&1; then + server_ready=true + fi if curl -fsS --max-time 10 "$models_url" 2>/dev/null \ | python3 -c ' import json @@ -2829,19 +2834,21 @@ raise SystemExit(0 if any(model.get("id") == expected for model in models) else chat_status="" if [ "$model_ready" = true ]; then - if [ "$model_ready_since" -lt 0 ]; then - model_ready_since=$SECONDS - fi chat_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \ "$chat_url" 2>/dev/null)" || true case "$chat_status" in 401|403|405) break ;; esac - if [ $((SECONDS - model_ready_since)) -ge "$stabilization_seconds" ]; then + fi + if [ "$server_ready" = true ]; then + if [ "$server_ready_since" -lt 0 ]; then + server_ready_since=$SECONDS + fi + if [ $((SECONDS - server_ready_since)) -ge "$stabilization_seconds" ]; then break fi else - model_ready_since=-1 + server_ready_since=-1 fi elapsed=$((SECONDS - start_seconds)) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 7c319f7dfc..acf98161db 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2542,19 +2542,22 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> ) events = events_path.read_text().splitlines() - assert events[0].endswith("http://localhost:8765/v1/models") - assert events[1].endswith("http://localhost:8765/v1/chat/completions") - assert "--data" not in events[1] + assert events[0].endswith("http://localhost:8765/") + assert events[1].endswith("http://localhost:8765/v1/models") + assert events[2].endswith("http://localhost:8765/v1/chat/completions") + assert "--data" not in events[2] -def test_chat_route_readiness_accepts_stable_registered_model(tmp_path: Path) -> None: +def test_chat_route_readiness_accepts_stable_server_with_different_model_id( + tmp_path: Path, +) -> None: bin_dir = tmp_path / "bin" bin_dir.mkdir() curl = bin_dir / "curl" curl.write_text( """#!/usr/bin/env bash case "$*" in - */v1/models*) printf '{"data":[{"id":"test-model"}]}\n' ;; + */v1/models*) printf '{"data":[{"id":"/models/different-model"}]}\n' ;; */v1/chat/completions*) printf '404' ;; esac """, From c12cdd656aa798fb8036012d4b3af3359a68261f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:48:58 -0500 Subject: [PATCH 77/99] fix: stabilize OpenAI health before eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:评估前等待 OpenAI 健康状态稳定 --- benchmarks/benchmark_lib.sh | 6 +++--- utils/evals/test_run_eval_dispatch.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 30231c090f..8960bde7a6 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -2782,7 +2782,7 @@ _wait_for_openai_chat_route() { local next_report=0 local elapsed percent chat_status local served_model="${SERVED_MODEL_NAME:-${MODEL:-}}" - local root_url models_url chat_url + local health_url models_url chat_url while [[ $# -gt 0 ]]; do case "$1" in @@ -2809,14 +2809,14 @@ _wait_for_openai_chat_route() { echo "ERROR: MODEL or SERVED_MODEL_NAME is required for chat endpoint readiness" >&2 return 2 fi - root_url="http://localhost:${port}/" + health_url="http://localhost:${port}/health" models_url="http://localhost:${port}/v1/models" chat_url="http://localhost:${port}/v1/chat/completions" while true; do local model_ready=false local server_ready=false - if curl -fsS --max-time 10 "$root_url" >/dev/null 2>&1; then + if curl -fsS --max-time 10 "$health_url" >/dev/null 2>&1; then server_ready=true fi if curl -fsS --max-time 10 "$models_url" 2>/dev/null \ diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index acf98161db..df15c7bdf2 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2542,7 +2542,7 @@ def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> ) events = events_path.read_text().splitlines() - assert events[0].endswith("http://localhost:8765/") + assert events[0].endswith("http://localhost:8765/health") assert events[1].endswith("http://localhost:8765/v1/models") assert events[2].endswith("http://localhost:8765/v1/chat/completions") assert "--data" not in events[2] From 4961aeccc13a045df4351cbd8f8c3eb1462c4bd4 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:52:04 -0500 Subject: [PATCH 78/99] fix: refresh expired MiniMax image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:更新已失效的 MiniMax 镜像标签 --- configs/nvidia-master.yaml | 4 ++-- perf-changelog.yaml | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/configs/nvidia-master.yaml b/configs/nvidia-master.yaml index 438a3e9da0..f5672cec3f 100644 --- a/configs/nvidia-master.yaml +++ b/configs/nvidia-master.yaml @@ -7451,7 +7451,7 @@ qwen3.5-fp4-gb200-dynamo-sglang-agentic-mtp: - "CONFIG_FILE=recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp-hicache.yaml" minimaxm3-fp4-b300-vllm-agentic-mtp: - image: vllm/vllm-openai:nightly-ac7509e2b1db40fec2f03dde1ed4e9dfdc2338c9 + image: vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45 model: nvidia/MiniMax-M3-NVFP4 model-prefix: minimaxm3 runner: cluster:b300-nv @@ -7605,7 +7605,7 @@ minimaxm3-fp4-b300-trtllm-agentic-mtp: # the same 3 TB AgentX ceiling before the proportional-GPU rule is applied. # GPU-resident points receive a zero budget. minimaxm3-fp4-b200-vllm-agentic-mtp: - image: vllm/vllm-openai:nightly-ac7509e2b1db40fec2f03dde1ed4e9dfdc2338c9 + image: vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45 model: nvidia/MiniMax-M3-NVFP4 model-prefix: minimaxm3 runner: cluster:b200-nscale diff --git a/perf-changelog.yaml b/perf-changelog.yaml index ab92e7921c..126263e5f5 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6576,3 +6576,12 @@ - "Recipes sourced from srt-slurm (recipes/trtllm/qwen3.5-fp4/inferencex/gb300/{mtp,stp})." - "Runner: launch_gb300-nv.sh bumped from NVIDIA/srt-slurm@v1.0.29 to v1.0.72 for the dynamo-trt+qwen3.5+fp4 path." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2730 + +- config-keys: + - minimaxm3-fp4-b300-vllm-agentic-mtp + - minimaxm3-fp4-b200-vllm-agentic-mtp + scenario-type: + - agentic-coding + description: + - "Replace the expired vLLM nightly image with the current pinned upstream nightly so MiniMax-M3 AgentX jobs can import their container instead of failing with Docker Hub HTTP 404." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 From f76459e9fbbbfbd74182c3fcaee6a8c8fc2f5aa3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:05:15 -0500 Subject: [PATCH 79/99] fix: allocate hybrid DP ranks correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:正确分配混合并行的数据并行 rank --- perf-changelog.yaml | 8 ++ runners/launch_gb200-nv.sh | 5 + runners/patch_srt_vllm_dp_ranks.py | 146 +++++++++++++++++++++++++++++ runners/test_slurm_utils.py | 49 ++++++++++ 4 files changed, 208 insertions(+) create mode 100755 runners/patch_srt_vllm_dp_ranks.py diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 126263e5f5..3176beeecd 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6585,3 +6585,11 @@ description: - "Replace the expired vLLM nightly image with the current pinned upstream nightly so MiniMax-M3 AgentX jobs can import their container instead of failing with Docker Hub HTTP 404." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 + +- config-keys: + - kimik3-fp4-gb200-dynamo-vllm-agentic + scenario-type: + - agentic-coding + description: + - "Correct srt-slurm's external data-parallel process allocation so Kimi-K3 TP4/DP4 workers receive four GPUs per DP rank instead of one invalid rank per GPU." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/runners/launch_gb200-nv.sh b/runners/launch_gb200-nv.sh index 73a9bb1dcb..0bc3413fda 100755 --- a/runners/launch_gb200-nv.sh +++ b/runners/launch_gb200-nv.sh @@ -442,6 +442,11 @@ elif [[ "$IS_AGENTIC" == "1" && (( "$MODEL_PREFIX" == "qwen3.5" && "$PRECISION" elif [[ "$IS_AGENTIC" == "1" && "$MODEL_PREFIX" == "kimik3" ]]; then git clone --branch v1.0.53 --single-branch https://github.com/NVIDIA/srt-slurm.git "$SRT_REPO_DIR" || exit 1 cd "$SRT_REPO_DIR" || exit 1 + test "$(git rev-parse HEAD)" = "217f94387abeddfed7149a71955dc523e07cd765" || { + echo "Error: NVIDIA/srt-slurm v1.0.53 resolved to an unexpected commit" >&2 + exit 1 + } + python3 "$GITHUB_WORKSPACE/runners/patch_srt_vllm_dp_ranks.py" "$(pwd)" || exit 1 mkdir -p recipes/vllm/kimi-k3/agentic || exit 1 cp -rT "$GITHUB_WORKSPACE/benchmarks/multi_node/srt-slurm-recipes/vllm/kimi-k3/agentic" \ recipes/vllm/kimi-k3/agentic || exit 1 diff --git a/runners/patch_srt_vllm_dp_ranks.py b/runners/patch_srt_vllm_dp_ranks.py new file mode 100755 index 0000000000..dbc11da40b --- /dev/null +++ b/runners/patch_srt_vllm_dp_ranks.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Patch srt-slurm v1.0.53 to allocate one process per vLLM DP rank.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +OLD_BLOCK = ''' else: + # DP+EP mode: one process per GPU + # Each process gets a single GPU and a unique dp_rank + dp_rank = 0 + # Allocate a unique DP RPC port for this endpoint's leader node + dp_rpc_port = port_allocator.next_dp_rpc_port(endpoint.leader_node) + # Allocate a single NIXL base port for this endpoint. + # vLLM internally computes: actual_port = base + data_parallel_rank + # so all DP ranks in the endpoint share the same base port. + dp_size = self._get_dp_size(endpoint.mode) or len(endpoint.gpu_indices) + nixl_base_port = port_allocator.next_nixl_port_block(dp_size) + for _node_rank, node in enumerate(endpoint.nodes): + for gpu_idx in sorted(endpoint.gpu_indices): + is_leader = dp_rank == 0 + http_port = port_allocator.next_http_port(node) if is_leader else 0 + bootstrap_port = ( + port_allocator.next_bootstrap_port(node) + if endpoint.mode == "prefill" and is_leader + else None + ) + kv_events_port = port_allocator.next_kv_events_port() + nixl_port = nixl_base_port + + processes.append( + Process( + node=node, + gpu_indices=frozenset([gpu_idx]), # Single GPU per process + sys_port=current_sys_port, + http_port=http_port, + endpoint_mode=endpoint.mode, + endpoint_index=endpoint.index, + node_rank=dp_rank, # dp_rank stored in node_rank for now + bootstrap_port=bootstrap_port, + kv_events_port=kv_events_port, + nixl_port=nixl_port, + dp_rpc_port=dp_rpc_port, + ) + ) + current_sys_port += 1 + dp_rank += 1 +''' + +NEW_BLOCK = ''' else: + # External DP mode: one process per DP rank. A rank may own + # multiple GPUs when tensor or pipeline parallelism is enabled. + dp_rank = 0 + dp_rpc_port = port_allocator.next_dp_rpc_port(endpoint.leader_node) + config = self.get_config_for_mode(endpoint.mode) + dp_size = self._get_dp_size(endpoint.mode) or endpoint.total_gpus + tp_size = config.get("tensor-parallel-size") or config.get("tensor_parallel_size") or 1 + pp_size = config.get("pipeline-parallel-size") or config.get("pipeline_parallel_size") or 1 + gpus_per_dp_rank = tp_size * pp_size + expected_gpus = dp_size * gpus_per_dp_rank + if endpoint.total_gpus != expected_gpus: + raise ValueError( + f"{endpoint.mode} DP={dp_size}, TP={tp_size}, PP={pp_size} requires " + f"{expected_gpus} GPUs, but the endpoint allocated {endpoint.total_gpus}" + ) + + nixl_base_port = port_allocator.next_nixl_port_block(dp_size) + for node in endpoint.nodes: + local_gpus = sorted(endpoint.gpu_indices) + if len(local_gpus) % gpus_per_dp_rank != 0: + raise ValueError( + f"{endpoint.mode} TP={tp_size}, PP={pp_size} requires " + f"{gpus_per_dp_rank} GPUs per DP rank, but node {node} has " + f"{len(local_gpus)} allocated GPUs" + ) + for offset in range(0, len(local_gpus), gpus_per_dp_rank): + rank_gpus = frozenset(local_gpus[offset : offset + gpus_per_dp_rank]) + is_leader = dp_rank == 0 + http_port = port_allocator.next_http_port(node) if is_leader else 0 + bootstrap_port = ( + port_allocator.next_bootstrap_port(node) + if endpoint.mode == "prefill" and is_leader + else None + ) + + processes.append( + Process( + node=node, + gpu_indices=rank_gpus, + sys_port=current_sys_port, + http_port=http_port, + endpoint_mode=endpoint.mode, + endpoint_index=endpoint.index, + node_rank=dp_rank, + bootstrap_port=bootstrap_port, + kv_events_port=port_allocator.next_kv_events_port(), + nixl_port=nixl_base_port, + dp_rpc_port=dp_rpc_port, + ) + ) + current_sys_port += 1 + dp_rank += 1 + + if dp_rank != dp_size: + raise ValueError( + f"{endpoint.mode} allocated {dp_rank} DP ranks, expected {dp_size}" + ) +''' + + +def patch_backend(root: Path) -> bool: + """Apply the rank allocator patch and return whether the file changed.""" + backend = root / "src/srtctl/backends/vllm.py" + source = backend.read_text() + + if NEW_BLOCK in source: + return False + if source.count(OLD_BLOCK) != 1: + raise RuntimeError( + f"unsupported srt-slurm vLLM backend at {backend}: expected allocation block not found exactly once" + ) + + backend.write_text(source.replace(OLD_BLOCK, NEW_BLOCK)) + return True + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print(f"Usage: {argv[0]} SRT_SLURM_CHECKOUT", file=sys.stderr) + return 2 + + try: + changed = patch_backend(Path(argv[1]).resolve()) + except (OSError, RuntimeError) as error: + print(f"ERROR: failed to patch srt-slurm vLLM DP ranks: {error}", file=sys.stderr) + return 1 + + state = "Patched" if changed else "Already patched" + print(f"{state} srt-slurm vLLM DP rank allocation") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 13867c2c73..1a426f6537 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -1,5 +1,6 @@ import json import os +import runpy import subprocess from pathlib import Path @@ -8,6 +9,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] SLURM_UTILS = REPO_ROOT / "runners" / "slurm_utils.sh" PATCH_SRT_EVAL = REPO_ROOT / "runners" / "patch_srt_eval_dispatch.py" +PATCH_SRT_DP_RANKS = REPO_ROOT / "runners" / "patch_srt_vllm_dp_ranks.py" INJECT_ACCEPTANCE = REPO_ROOT / "runners" / "inject_synthetic_acceptance.py" @@ -114,6 +116,53 @@ def test_patch_srt_eval_dispatch_forwards_selection_and_is_idempotent( assert "already patched" in second.stdout +def test_patch_srt_vllm_dp_ranks_groups_tensor_parallel_devices( + tmp_path: Path, +) -> None: + symbols = runpy.run_path(str(PATCH_SRT_DP_RANKS)) + backend = tmp_path / "src/srtctl/backends/vllm.py" + backend.parent.mkdir(parents=True) + backend.write_text(f"prefix\n{symbols['OLD_BLOCK']}suffix\n") + + first = subprocess.run( + ["python3", str(PATCH_SRT_DP_RANKS), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + second = subprocess.run( + ["python3", str(PATCH_SRT_DP_RANKS), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + + patched = backend.read_text() + assert first.returncode == 0, first.stderr + assert second.returncode == 0, second.stderr + assert symbols["OLD_BLOCK"] not in patched + assert patched.count(symbols["NEW_BLOCK"]) == 1 + assert "gpus_per_dp_rank = tp_size * pp_size" in patched + assert "gpu_indices=rank_gpus" in patched + assert "already patched" in second.stdout.lower() + + +def test_patch_srt_vllm_dp_ranks_rejects_unknown_source(tmp_path: Path) -> None: + backend = tmp_path / "src/srtctl/backends/vllm.py" + backend.parent.mkdir(parents=True) + backend.write_text("unsupported backend\n") + + result = subprocess.run( + ["python3", str(PATCH_SRT_DP_RANKS), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert backend.read_text() == "unsupported backend\n" + + def test_patch_srt_eval_dispatch_preflights_before_writing(tmp_path: Path) -> None: do_sweep = tmp_path / "src/srtctl/cli/do_sweep.py" eval_script = tmp_path / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh" From dcc27634c046145631f286e56e6c369fbac36714 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:14:45 -0500 Subject: [PATCH 80/99] fix: accept stock TensorRT chat requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:兼容 stock 验证器发送的 TensorRT-LLM 聊天请求 --- .../agentic/minimaxm3_fp4_b200_trt_mtp.sh | 4 + .../agentic/minimaxm3_fp4_b300_trt_mtp.sh | 4 + perf-changelog.yaml | 9 ++ runners/patch_trtllm_chat_store.py | 84 +++++++++++++++++++ runners/test_slurm_utils.py | 61 ++++++++++++++ 5 files changed, 162 insertions(+) create mode 100755 runners/patch_trtllm_chat_store.py diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh index c83b53aef2..81e0349089 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh @@ -44,6 +44,10 @@ nvidia-smi resolve_trace_source install_agentic_deps +# BFCL's stock OpenAI client sends the standard `store=false` field. TRT-LLM +# 1.3 rejects that field even though this server never persists responses. +python3 "$(dirname "$0")/../../../runners/patch_trtllm_chat_store.py" + # kv_cache_config.host_cache_size is pinned per topology in ser.yaml below # (200 GiB at TP8, 250 GiB at TP4 -- see $mem_off), NOT derived from # TOTAL_CPU_DRAM_GB. diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh index aebc8390d3..0ce9d0abb1 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh @@ -43,6 +43,10 @@ nvidia-smi resolve_trace_source install_agentic_deps +# BFCL's stock OpenAI client sends the standard `store=false` field. TRT-LLM +# 1.3 rejects that field even though this server never persists responses. +python3 "$(dirname "$0")/../../../runners/patch_trtllm_chat_store.py" + SERVER_LOG="$RESULT_DIR/server.log" mkdir -p "$RESULT_DIR" diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 3176beeecd..70cc091018 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6593,3 +6593,12 @@ description: - "Correct srt-slurm's external data-parallel process allocation so Kimi-K3 TP4/DP4 workers receive four GPUs per DP rank instead of one invalid rank per GPU." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 + +- config-keys: + - minimaxm3-fp4-b300-trtllm-agentic-mtp + - minimaxm3-fp4-b200-trtllm-agentic-mtp + scenario-type: + - agentic-coding + description: + - "Accept the stock BFCL client's standard store=false chat-completion field instead of rejecting every TensorRT-LLM tool-use request with HTTP 400." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/runners/patch_trtllm_chat_store.py b/runners/patch_trtllm_chat_store.py new file mode 100755 index 0000000000..613f368560 --- /dev/null +++ b/runners/patch_trtllm_chat_store.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Accept OpenAI's non-persistent chat request field in TensorRT-LLM.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +CLASS_HEADER = "class ChatCompletionRequest(OpenAIBaseModel):\n" +NEXT_CLASS = "\nclass " +FIELD_ANCHOR = " stream: Optional[bool] = False\n" +STORE_FIELD = " store: Optional[Literal[False]] = False\n" + + +def installed_protocol_path() -> Path: + """Return the protocol module path from the installed TensorRT-LLM package.""" + spec = importlib.util.find_spec("tensorrt_llm") + if spec is None or not spec.submodule_search_locations: + raise RuntimeError("tensorrt_llm package is not installed") + package_root = Path(next(iter(spec.submodule_search_locations))) + return package_root / "serve/openai_protocol.py" + + +def patch_protocol(protocol_path: Path) -> bool: + """Accept only ``store=false`` and return whether the source changed.""" + source = protocol_path.read_text() + if source.count(CLASS_HEADER) != 1: + raise RuntimeError( + f"unsupported TensorRT-LLM protocol at {protocol_path}: " + "ChatCompletionRequest not found exactly once" + ) + + class_start = source.index(CLASS_HEADER) + class_end = source.find(NEXT_CLASS, class_start + len(CLASS_HEADER)) + if class_end == -1: + raise RuntimeError( + f"unsupported TensorRT-LLM protocol at {protocol_path}: " + "ChatCompletionRequest boundary not found" + ) + + class_source = source[class_start:class_end] + if STORE_FIELD in class_source: + return False + if " store:" in class_source: + raise RuntimeError( + f"unsupported TensorRT-LLM store field at {protocol_path}" + ) + if class_source.count(FIELD_ANCHOR) != 1: + raise RuntimeError( + f"unsupported TensorRT-LLM protocol at {protocol_path}: " + "ChatCompletionRequest stream field not found exactly once" + ) + + patched_class = class_source.replace( + FIELD_ANCHOR, + STORE_FIELD + FIELD_ANCHOR, + ) + protocol_path.write_text(source[:class_start] + patched_class + source[class_end:]) + return True + + +def main(argv: list[str]) -> int: + if len(argv) > 2: + print(f"Usage: {argv[0]} [OPENAI_PROTOCOL_PATH]", file=sys.stderr) + return 2 + + try: + protocol_path = ( + Path(argv[1]).resolve() if len(argv) == 2 else installed_protocol_path() + ) + changed = patch_protocol(protocol_path) + except (OSError, RuntimeError) as error: + print(f"ERROR: failed to patch TensorRT-LLM chat requests: {error}", file=sys.stderr) + return 1 + + state = "Patched" if changed else "Already patched" + print(f"{state} TensorRT-LLM ChatCompletionRequest store=false support") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 1a426f6537..256922dde0 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -10,6 +10,7 @@ SLURM_UTILS = REPO_ROOT / "runners" / "slurm_utils.sh" PATCH_SRT_EVAL = REPO_ROOT / "runners" / "patch_srt_eval_dispatch.py" PATCH_SRT_DP_RANKS = REPO_ROOT / "runners" / "patch_srt_vllm_dp_ranks.py" +PATCH_TRTLLM_CHAT_STORE = REPO_ROOT / "runners" / "patch_trtllm_chat_store.py" INJECT_ACCEPTANCE = REPO_ROOT / "runners" / "inject_synthetic_acceptance.py" @@ -163,6 +164,66 @@ def test_patch_srt_vllm_dp_ranks_rejects_unknown_source(tmp_path: Path) -> None: assert backend.read_text() == "unsupported backend\n" + +def test_patch_trtllm_chat_store_accepts_false_and_is_idempotent( + tmp_path: Path, +) -> None: + protocol = tmp_path / "openai_protocol.py" + protocol.write_text( + "from typing import Literal, Optional\n\n" + "class ChatCompletionRequest(OpenAIBaseModel):\n" + " messages: list\n" + " stream: Optional[bool] = False\n" + " user: Optional[str] = None\n\n" + "class ResponsesRequest(OpenAIBaseModel):\n" + " store: Optional[bool] = True\n" + ) + + first = subprocess.run( + ["python3", str(PATCH_TRTLLM_CHAT_STORE), str(protocol)], + check=False, + capture_output=True, + text=True, + ) + second = subprocess.run( + ["python3", str(PATCH_TRTLLM_CHAT_STORE), str(protocol)], + check=False, + capture_output=True, + text=True, + ) + + patched = protocol.read_text() + assert first.returncode == 0, first.stderr + assert second.returncode == 0, second.stderr + assert patched.count("store: Optional[Literal[False]] = False") == 1 + assert patched.count("store: Optional[bool] = True") == 1 + assert "already patched" in second.stdout.lower() + + +def test_patch_trtllm_chat_store_rejects_unknown_source(tmp_path: Path) -> None: + protocol = tmp_path / "openai_protocol.py" + protocol.write_text("unsupported protocol\n") + + result = subprocess.run( + ["python3", str(PATCH_TRTLLM_CHAT_STORE), str(protocol)], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert protocol.read_text() == "unsupported protocol\n" + + +def test_minimax_trt_launchers_patch_chat_store_request() -> None: + launchers = ( + REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b200_trt_mtp.sh", + REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b300_trt_mtp.sh", + ) + + for launcher in launchers: + assert "patch_trtllm_chat_store.py" in launcher.read_text(), launcher + def test_patch_srt_eval_dispatch_preflights_before_writing(tmp_path: Path) -> None: do_sweep = tmp_path / "src/srtctl/cli/do_sweep.py" eval_script = tmp_path / "src/srtctl/benchmarks/scripts/lm-eval/bench.sh" From 5aec415744c6351505c4d450efb515fd7c410e8d Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:23:50 -0500 Subject: [PATCH 81/99] fix: keep AgentX dependency setup rootless MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保持 AgentX 依赖安装无需 root 权限 --- benchmarks/benchmark_lib.sh | 9 ++--- perf-changelog.yaml | 9 +++++ utils/evals/test_run_eval_dispatch.py | 48 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 8960bde7a6..97c8933aaf 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -3105,13 +3105,8 @@ install_agentic_deps() { return fi - # AIPerf must not share site-packages with the inference server. Installing - # it into vLLM/SGLang's system Python can upgrade FastAPI, Starlette, - # transformers, or other packages while the server imports from that same - # environment. - if ! command -v git >/dev/null 2>&1; then - apt-get update && apt-get install -y git - fi + # Install from the checked-out aiperf source with uv. This path does not + # require git, and rootless Enroot containers cannot mutate dpkg. ensure_agentic_uv rm -rf "$AIPERF_VENV" diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 70cc091018..571f00fa77 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6602,3 +6602,12 @@ description: - "Accept the stock BFCL client's standard store=false chat-completion field instead of rejecting every TensorRT-LLM tool-use request with HTTP 400." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 + +- config-keys: + - minimaxm3-fp4-b300-vllm-agentic-mtp + - minimaxm3-fp4-b200-vllm-agentic-mtp + scenario-type: + - agentic-coding + description: + - "Keep AgentX dependency setup rootless so minimal vLLM images without git do not fail while attempting a privileged apt install." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index df15c7bdf2..1b2c2ce253 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -70,6 +70,54 @@ def _dispatch( return res.stdout +def test_agentic_dependency_install_is_rootless_without_git(tmp_path: Path) -> None: + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_uv = fake_bin / "uv" + fake_uv.write_text( + "#!/bin/bash\n" + "if [[ \"$1\" == \"venv\" ]]; then\n" + " target=\"${@: -1}\"\n" + " mkdir -p \"$target/bin\"\n" + " printf '#!/bin/sh\\nexit 0\\n' > \"$target/bin/aiperf\"\n" + " printf '#!/bin/sh\\nexit 0\\n' > \"$target/bin/hf\"\n" + " chmod +x \"$target/bin/aiperf\" \"$target/bin/hf\"\n" + "fi\n" + ) + fake_uv.chmod(0o755) + fake_apt = fake_bin / "apt-get" + fake_apt.write_text("#!/bin/sh\nexit 97\n") + fake_apt.chmod(0o755) + + env = { + **os.environ, + "AGENTIC_DIR": str(tmp_path / "agentic"), + "AIPERF_DIR": str(tmp_path / "aiperf"), + "AIPERF_RUNTIME_DIR": str(tmp_path / "runtime"), + "BENCHMARK_LIB": str(BENCHMARK_LIB), + "FAKE_UV": str(fake_uv), + "PATH": f"{fake_bin}:/bin", + "PYTHONPYCACHEPREFIX": str(tmp_path / "pycache"), + } + result = subprocess.run( + [ + "/bin/bash", + "-c", + 'source "$BENCHMARK_LIB"; ' + 'ensure_agentic_uv() { AIPERF_UV_BIN="$FAKE_UV"; }; ' + "install_agentic_deps", + ], + env=env, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert (tmp_path / "runtime/venv/bin/aiperf").is_file() + assert (tmp_path / "runtime/venv/bin/hf").is_file() + + def test_agentic_scenario_defaults_to_gsm8k_lm_eval(): assert "DISPATCH=lm-eval" in _dispatch(is_agentic="1") From b4fdc498fa0ad225692192d5487505abfb6febf3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:41:29 -0500 Subject: [PATCH 82/99] fix: support packaged MiniMax evaluator imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:兼容以 Python 包方式导入 MiniMax 评估器 --- utils/evals/minimax_provider_eval.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 1e9a3b7670..33a2e87bae 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -15,7 +15,10 @@ from pathlib import Path from typing import Any -from minimax_m3_full_eval import UPSTREAM_REF, verify_source_tree +if __package__: + from .minimax_m3_full_eval import UPSTREAM_REF, verify_source_tree +else: + from minimax_m3_full_eval import UPSTREAM_REF, verify_source_tree TASK_NAME = "minimax_m3_smoke" NATIVE_REPORT_FILENAME = "minimax_vendor_report.json" From 19bb70a0c9b607e8a91fb0174a91faed22dbf60f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:02:48 -0500 Subject: [PATCH 83/99] fix: size vLLM offload allocations correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:正确计算 vLLM 卸载缓存分配大小 --- .../agentic/minimaxm3_fp4_b200_mtp.sh | 1 + perf-changelog.yaml | 8 ++ runners/patch_vllm_simple_kv_offload.py | 78 +++++++++++++++++++ runners/test_slurm_utils.py | 59 ++++++++++++++ 4 files changed, 146 insertions(+) create mode 100755 runners/patch_vllm_simple_kv_offload.py diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh index 2bd9f45567..8dd5807d23 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh @@ -126,6 +126,7 @@ install_agentic_deps OFFLOAD_ARGS=() if require_agentic_kv_offload_backend vllm-simple; then + python3 "$(dirname "$0")/../../../runners/patch_vllm_simple_kv_offload.py" CPU_OFFLOAD_BYTES=$((TOTAL_CPU_DRAM_GB * 1024 * 1024 * 1024)) export VLLM_USE_SIMPLE_KV_OFFLOAD=1 OFFLOAD_CONFIG=$(printf \ diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 571f00fa77..385df5382c 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6611,3 +6611,11 @@ description: - "Keep AgentX dependency setup rootless so minimal vLLM images without git do not fail while attempting a privileged apt install." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 + +- config-keys: + - minimaxm3-fp4-b200-vllm-agentic-mtp + scenario-type: + - agentic-coding + description: + - "Size each vLLM SimpleCPUOffload backing allocation from its own KV-cache metadata so hybrid MiniMax-M3 cache layouts initialize without invalid tensor views." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/runners/patch_vllm_simple_kv_offload.py b/runners/patch_vllm_simple_kv_offload.py new file mode 100755 index 0000000000..62be471713 --- /dev/null +++ b/runners/patch_vllm_simple_kv_offload.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Use each vLLM KV allocation's own logical size during CPU offload setup.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +OLD_BLOCK = """ logical_storage_bytes = self.kv_cache_config.kv_cache_tensors[0].size + + # The DMA backend copies whole blocks as base + block_id * stride(0), +""" +NEW_BLOCK = """ logical_storage_bytes_by_layer = { + layer: cache_tensor.size + for cache_tensor in self.kv_cache_config.kv_cache_tensors + for layer in cache_tensor.layers + } + + # The DMA backend copies whole blocks as base + block_id * stride(0), +""" +OLD_STORAGE = """ storage = tensor.untyped_storage() + key = (tensor.device, storage.data_ptr()) +""" +NEW_STORAGE = """ logical_storage_bytes = logical_storage_bytes_by_layer[name] + storage = tensor.untyped_storage() + key = (tensor.device, storage.data_ptr()) +""" + + +def installed_worker_path() -> Path: + """Return the SimpleCPUOffload worker module from the installed vLLM.""" + spec = importlib.util.find_spec("vllm") + if spec is None or not spec.submodule_search_locations: + raise RuntimeError("vllm package is not installed") + package_root = Path(next(iter(spec.submodule_search_locations))) + return package_root / "v1/simple_kv_offload/worker.py" + + +def patch_worker(worker_path: Path) -> bool: + """Patch per-allocation sizing and return whether the source changed.""" + source = worker_path.read_text() + if NEW_BLOCK in source and NEW_STORAGE in source: + return False + if NEW_BLOCK in source or NEW_STORAGE in source: + raise RuntimeError(f"partially patched vLLM worker at {worker_path}") + if source.count(OLD_BLOCK) != 1 or source.count(OLD_STORAGE) != 1: + raise RuntimeError( + f"unsupported vLLM SimpleCPUOffload worker at {worker_path}" + ) + + patched = source.replace(OLD_BLOCK, NEW_BLOCK).replace(OLD_STORAGE, NEW_STORAGE) + worker_path.write_text(patched) + return True + + +def main(argv: list[str]) -> int: + if len(argv) > 2: + print(f"Usage: {argv[0]} [WORKER_PATH]", file=sys.stderr) + return 2 + + try: + worker_path = ( + Path(argv[1]).resolve() if len(argv) == 2 else installed_worker_path() + ) + changed = patch_worker(worker_path) + except (OSError, RuntimeError) as error: + print(f"ERROR: failed to patch vLLM CPU offload: {error}", file=sys.stderr) + return 1 + + state = "Patched" if changed else "Already patched" + print(f"{state} vLLM SimpleCPUOffload per-allocation sizing") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 256922dde0..0578408418 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -11,6 +11,7 @@ PATCH_SRT_EVAL = REPO_ROOT / "runners" / "patch_srt_eval_dispatch.py" PATCH_SRT_DP_RANKS = REPO_ROOT / "runners" / "patch_srt_vllm_dp_ranks.py" PATCH_TRTLLM_CHAT_STORE = REPO_ROOT / "runners" / "patch_trtllm_chat_store.py" +PATCH_VLLM_SIMPLE_KV = REPO_ROOT / "runners" / "patch_vllm_simple_kv_offload.py" INJECT_ACCEPTANCE = REPO_ROOT / "runners" / "inject_synthetic_acceptance.py" @@ -214,6 +215,64 @@ def test_patch_trtllm_chat_store_rejects_unknown_source(tmp_path: Path) -> None: assert result.returncode == 1 assert protocol.read_text() == "unsupported protocol\n" +def test_patch_vllm_simple_kv_offload_sizes_each_allocation( + tmp_path: Path, +) -> None: + symbols = runpy.run_path(str(PATCH_VLLM_SIMPLE_KV)) + worker = tmp_path / "worker.py" + worker.write_text( + f"prefix\n{symbols['OLD_BLOCK']}" + " for name, tensor in kv_caches.items():\n" + f"{symbols['OLD_STORAGE']}suffix\n" + ) + + first = subprocess.run( + ["python3", str(PATCH_VLLM_SIMPLE_KV), str(worker)], + check=False, + capture_output=True, + text=True, + ) + second = subprocess.run( + ["python3", str(PATCH_VLLM_SIMPLE_KV), str(worker)], + check=False, + capture_output=True, + text=True, + ) + + patched = worker.read_text() + assert first.returncode == 0, first.stderr + assert second.returncode == 0, second.stderr + assert symbols["OLD_BLOCK"] not in patched + assert patched.count(symbols["NEW_BLOCK"]) == 1 + assert patched.count(symbols["NEW_STORAGE"]) == 1 + assert "already patched" in second.stdout.lower() + + +def test_patch_vllm_simple_kv_offload_rejects_unknown_source( + tmp_path: Path, +) -> None: + worker = tmp_path / "worker.py" + worker.write_text("unsupported worker\n") + + result = subprocess.run( + ["python3", str(PATCH_VLLM_SIMPLE_KV), str(worker)], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert worker.read_text() == "unsupported worker\n" + + +def test_minimax_b200_vllm_launcher_patches_simple_kv_offload() -> None: + launcher = ( + REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh" + ) + + assert "patch_vllm_simple_kv_offload.py" in launcher.read_text() + + def test_minimax_trt_launchers_patch_chat_store_request() -> None: launchers = ( From 64e85a8754aa0bc504e2b98c35244d5ad5735e08 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:09:57 -0500 Subject: [PATCH 84/99] fix: split heterogeneous vLLM cache layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:按层拆分异构 vLLM KV 缓存以支持卸载 --- perf-changelog.yaml | 2 +- runners/patch_vllm_simple_kv_offload.py | 86 ++++++++++++++++++++----- runners/test_slurm_utils.py | 15 +++-- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 385df5382c..8565677acd 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6617,5 +6617,5 @@ scenario-type: - agentic-coding description: - - "Size each vLLM SimpleCPUOffload backing allocation from its own KV-cache metadata so hybrid MiniMax-M3 cache layouts initialize without invalid tensor views." + - "Split heterogeneous vLLM layer-compact KV backing storage into per-layer offload regions so MiniMax-M3 cache layouts initialize without invalid tensor views." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/runners/patch_vllm_simple_kv_offload.py b/runners/patch_vllm_simple_kv_offload.py index 62be471713..f2b12852de 100755 --- a/runners/patch_vllm_simple_kv_offload.py +++ b/runners/patch_vllm_simple_kv_offload.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Use each vLLM KV allocation's own logical size during CPU offload setup.""" +"""Split heterogeneous vLLM KV backing storage into valid CPU offload regions.""" from __future__ import annotations @@ -8,24 +8,80 @@ from pathlib import Path -OLD_BLOCK = """ logical_storage_bytes = self.kv_cache_config.kv_cache_tensors[0].size +OLD_SETUP = """ logical_storage_bytes = self.kv_cache_config.kv_cache_tensors[0].size # The DMA backend copies whole blocks as base + block_id * stride(0), """ -NEW_BLOCK = """ logical_storage_bytes_by_layer = { - layer: cache_tensor.size +NEW_SETUP = """ logical_storage_bytes = self.kv_cache_config.kv_cache_tensors[0].size + split_storage_by_layer = any( + logical_storage_bytes + % (num_blocks * cache_tensor.block_stride) + != 0 for cache_tensor in self.kv_cache_config.kv_cache_tensors - for layer in cache_tensor.layers - } + ) # The DMA backend copies whole blocks as base + block_id * stride(0), """ -OLD_STORAGE = """ storage = tensor.untyped_storage() +OLD_LOOP = """ unique_gpu_caches: dict[str, torch.Tensor] = {} + seen: set[tuple[torch.device, int]] = set() + for name, tensor in kv_caches.items(): + storage = tensor.untyped_storage() key = (tensor.device, storage.data_ptr()) + if key in seen: + continue + seen.add(key) + + physical_per_block, remainder = divmod(tensor.shape[0], num_blocks) + assert remainder == 0, ( + f"KV cache {name!r} has {tensor.shape[0]} physical blocks, which " + f"is not divisible by {num_blocks} scheduler blocks" + ) + block_bytes = tensor.stride(0) * tensor.element_size() * physical_per_block + raw = torch.empty(0, dtype=torch.int8, device=tensor.device).set_(storage) + assert raw.numel() >= logical_storage_bytes, ( + f"KV cache {name!r} storage has {raw.numel()} bytes, smaller " + f"than the configured {logical_storage_bytes}-byte allocation" + ) + regions = raw[:logical_storage_bytes].view(-1, num_blocks, block_bytes) + for idx, region in enumerate(regions): + key_name = name if len(regions) == 1 else f"{name}.{idx}" + unique_gpu_caches[key_name] = region """ -NEW_STORAGE = """ logical_storage_bytes = logical_storage_bytes_by_layer[name] +NEW_LOOP = """ unique_gpu_caches: dict[str, torch.Tensor] = {} + seen: set[tuple[torch.device, int, int, int]] = set() + for name, tensor in kv_caches.items(): + physical_per_block, remainder = divmod(tensor.shape[0], num_blocks) + assert remainder == 0, ( + f"KV cache {name!r} has {tensor.shape[0]} physical blocks, which " + f"is not divisible by {num_blocks} scheduler blocks" + ) + block_bytes = tensor.stride(0) * tensor.element_size() * physical_per_block storage = tensor.untyped_storage() - key = (tensor.device, storage.data_ptr()) + raw = torch.empty(0, dtype=torch.int8, device=tensor.device).set_(storage) + + if split_storage_by_layer: + region_offset = tensor.storage_offset() * tensor.element_size() + region_bytes = num_blocks * block_bytes + else: + region_offset = 0 + region_bytes = logical_storage_bytes + + key = (tensor.device, storage.data_ptr(), region_offset, region_bytes) + if key in seen: + continue + seen.add(key) + + region_end = region_offset + region_bytes + assert raw.numel() >= region_end, ( + f"KV cache {name!r} storage has {raw.numel()} bytes, smaller " + f"than the required {region_end}-byte region" + ) + regions = raw[region_offset:region_end].view( + -1, num_blocks, block_bytes + ) + for idx, region in enumerate(regions): + key_name = name if len(regions) == 1 else f"{name}.{idx}" + unique_gpu_caches[key_name] = region """ @@ -39,18 +95,18 @@ def installed_worker_path() -> Path: def patch_worker(worker_path: Path) -> bool: - """Patch per-allocation sizing and return whether the source changed.""" + """Patch heterogeneous layer-region sizing and return whether source changed.""" source = worker_path.read_text() - if NEW_BLOCK in source and NEW_STORAGE in source: + if NEW_SETUP in source and NEW_LOOP in source: return False - if NEW_BLOCK in source or NEW_STORAGE in source: + if NEW_SETUP in source or NEW_LOOP in source: raise RuntimeError(f"partially patched vLLM worker at {worker_path}") - if source.count(OLD_BLOCK) != 1 or source.count(OLD_STORAGE) != 1: + if source.count(OLD_SETUP) != 1 or source.count(OLD_LOOP) != 1: raise RuntimeError( f"unsupported vLLM SimpleCPUOffload worker at {worker_path}" ) - patched = source.replace(OLD_BLOCK, NEW_BLOCK).replace(OLD_STORAGE, NEW_STORAGE) + patched = source.replace(OLD_SETUP, NEW_SETUP).replace(OLD_LOOP, NEW_LOOP) worker_path.write_text(patched) return True @@ -70,7 +126,7 @@ def main(argv: list[str]) -> int: return 1 state = "Patched" if changed else "Already patched" - print(f"{state} vLLM SimpleCPUOffload per-allocation sizing") + print(f"{state} vLLM SimpleCPUOffload heterogeneous layer regions") return 0 diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 0578408418..823eb33c1b 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -215,15 +215,13 @@ def test_patch_trtllm_chat_store_rejects_unknown_source(tmp_path: Path) -> None: assert result.returncode == 1 assert protocol.read_text() == "unsupported protocol\n" -def test_patch_vllm_simple_kv_offload_sizes_each_allocation( +def test_patch_vllm_simple_kv_offload_splits_heterogeneous_layers( tmp_path: Path, ) -> None: symbols = runpy.run_path(str(PATCH_VLLM_SIMPLE_KV)) worker = tmp_path / "worker.py" worker.write_text( - f"prefix\n{symbols['OLD_BLOCK']}" - " for name, tensor in kv_caches.items():\n" - f"{symbols['OLD_STORAGE']}suffix\n" + f"prefix\n{symbols['OLD_SETUP']}{symbols['OLD_LOOP']}suffix\n" ) first = subprocess.run( @@ -242,9 +240,12 @@ def test_patch_vllm_simple_kv_offload_sizes_each_allocation( patched = worker.read_text() assert first.returncode == 0, first.stderr assert second.returncode == 0, second.stderr - assert symbols["OLD_BLOCK"] not in patched - assert patched.count(symbols["NEW_BLOCK"]) == 1 - assert patched.count(symbols["NEW_STORAGE"]) == 1 + assert symbols["OLD_SETUP"] not in patched + assert symbols["OLD_LOOP"] not in patched + assert patched.count(symbols["NEW_SETUP"]) == 1 + assert patched.count(symbols["NEW_LOOP"]) == 1 + assert "split_storage_by_layer" in patched + assert "tensor.storage_offset() * tensor.element_size()" in patched assert "already patched" in second.stdout.lower() From 6b0c5db1a94381234163e5f173621402857d8374 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:11:31 -0500 Subject: [PATCH 85/99] chore: normalize vLLM offload patcher imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:规范 vLLM 卸载补丁脚本的导入格式 --- runners/patch_vllm_simple_kv_offload.py | 1 - 1 file changed, 1 deletion(-) diff --git a/runners/patch_vllm_simple_kv_offload.py b/runners/patch_vllm_simple_kv_offload.py index f2b12852de..872bd9960a 100755 --- a/runners/patch_vllm_simple_kv_offload.py +++ b/runners/patch_vllm_simple_kv_offload.py @@ -7,7 +7,6 @@ import sys from pathlib import Path - OLD_SETUP = """ logical_storage_bytes = self.kv_cache_config.kv_cache_tensors[0].size # The DMA backend copies whole blocks as base + block_id * stride(0), From bd139166bfea0b611e1a7c3a9a58782603d4e133 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:16:41 -0500 Subject: [PATCH 86/99] fix: patch B300 vLLM offload startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:在 B300 vLLM 卸载启动流程中应用缓存补丁 --- .../single_node/agentic/minimaxm3_fp4_b300_mtp.sh | 1 + perf-changelog.yaml | 1 + runners/test_slurm_utils.py | 11 ++++++----- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh index 1b65687c18..91accfee80 100755 --- a/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh +++ b/benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh @@ -41,6 +41,7 @@ install_agentic_deps OFFLOAD_ARGS=() if require_agentic_kv_offload_backend vllm-simple; then + python3 "$(dirname "$0")/../../../runners/patch_vllm_simple_kv_offload.py" CPU_OFFLOAD_BYTES=$((TOTAL_CPU_DRAM_GB * 1024 * 1024 * 1024)) export VLLM_USE_SIMPLE_KV_OFFLOAD=1 OFFLOAD_CONFIG=$(printf \ diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 8565677acd..3cf7b12f3c 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6614,6 +6614,7 @@ - config-keys: - minimaxm3-fp4-b200-vllm-agentic-mtp + - minimaxm3-fp4-b300-vllm-agentic-mtp scenario-type: - agentic-coding description: diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 823eb33c1b..6e0f18da09 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -266,13 +266,14 @@ def test_patch_vllm_simple_kv_offload_rejects_unknown_source( assert worker.read_text() == "unsupported worker\n" -def test_minimax_b200_vllm_launcher_patches_simple_kv_offload() -> None: - launcher = ( - REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh" +def test_minimax_vllm_launchers_patch_simple_kv_offload() -> None: + launchers = ( + REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b200_mtp.sh", + REPO_ROOT / "benchmarks/single_node/agentic/minimaxm3_fp4_b300_mtp.sh", ) - assert "patch_vllm_simple_kv_offload.py" in launcher.read_text() - + for launcher in launchers: + assert "patch_vllm_simple_kv_offload.py" in launcher.read_text(), launcher def test_minimax_trt_launchers_patch_chat_store_request() -> None: From d5bde4ce21f708ad476d1ca0cc2e72f6e8ea7050 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:52:56 -0500 Subject: [PATCH 87/99] fix: update validators and repair smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:更新验证器版本并修复 MiniMax smoke 适配逻辑 --- benchmarks/benchmark_lib.sh | 4 +- utils/evals/EVALS.md | 6 +- utils/evals/kimi_vendor_eval.py | 1 - utils/evals/minimax_m3_full_eval.py | 2 +- utils/evals/minimax_m3_smoke.json | 5 +- utils/evals/minimax_provider_eval.py | 32 +++++++--- utils/evals/test_minimax_provider_eval.py | 10 +++- utils/evals/test_run_eval_dispatch.py | 71 ++++++++++------------- 8 files changed, 70 insertions(+), 61 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 97c8933aaf..c60de078b5 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1323,8 +1323,8 @@ _run_kimi_tool_call_schema_eval() { local port="${PORT:-8888}" local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}" local verifier_repo="https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" - local verifier_ref="b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" - local verifier_archive_sha256="ab933117c894a785978f8aee0f052e5a9096b3029e7962354b1c07ea430588c3" + local verifier_ref="3dad65a760a8867cda72f6dd8848d876a4e851b4" + local verifier_archive_sha256="ede9ea300c72ccfde9d8975ea4b1b54e423c7625690f6631ab1e65a715821e01" local eval_suite="${EVAL_SUITE:-kimi_tool_call_schema}" local timeout_seconds=900 if [ "$eval_suite" = "kimi_tool_call_schema_full" ]; then diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 9ef230c419..9c91777453 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -128,7 +128,7 @@ launch their existing `*_mtp.sh` server instead of silently falling back to STP. The smoke runs the unmodified [MoonshotAI/Kimi-Vendor-Verifier](https://github.com/MoonshotAI/Kimi-Vendor-Verifier) -at commit `b9ed3a6665bdff2c943246f7d2903cd003d6ddd6`. Each run downloads and +at commit `3dad65a760a8867cda72f6dd8848d876a4e851b4`. Each run downloads and SHA256-verifies the fresh pinned GitHub source archive, then safely extracts only the upstream pytest configuration, tool-call schema tests, and bundled Walle cases. InferenceX does not install the verifier package or reimplement @@ -205,7 +205,7 @@ python3 utils/evals/validate_scores.py `utils/evals/minimax_m3_smoke.json` is derived from [MiniMax-AI/MiniMax-Provider-Verifier](https://github.com/MiniMax-AI/MiniMax-Provider-Verifier) `sample.jsonl` at commit -`85bf180e54e2ab0b31595cfdc697116c4760876d`. The vendored fixture retains +`c899f95e17bfc4a338ddd4cb1638279125885e55`. The vendored fixture retains the full upstream MIT copyright, permission, and warranty notice. It contains only upstream zero-based row 71, an `expected_tool_call: true` request exercising tool-call trigger and argument-schema validation. @@ -258,7 +258,7 @@ python3 utils/evals/validate_scores.py The runner downloads only the eight source and validator files allowlisted in `utils/evals/minimax_m3_full_eval.py` at commit -`85bf180e54e2ab0b31595cfdc697116c4760876d`, verifies each SHA256, and executes +`c899f95e17bfc4a338ddd4cb1638279125885e55`, verifies each SHA256, and executes the pinned `verify.py` once. It uses five workers, a 600-second request timeout, three upstream retries, and a seven-hour whole-suite timeout. The workflow retains at least one hour for artifact staging, score validation, and cleanup. diff --git a/utils/evals/kimi_vendor_eval.py b/utils/evals/kimi_vendor_eval.py index 4482bd21fa..ab09059910 100755 --- a/utils/evals/kimi_vendor_eval.py +++ b/utils/evals/kimi_vendor_eval.py @@ -339,7 +339,6 @@ def run_evaluation( timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, ) -> bool: """Run upstream pytest and always attempt to publish a compatibility result.""" - expected_total = _expected_total(task_name) output_dir.mkdir(parents=True, exist_ok=True) native_report = output_dir / NATIVE_REPORT_FILENAME compatibility_path = prepare_compatibility_path(output_dir) diff --git a/utils/evals/minimax_m3_full_eval.py b/utils/evals/minimax_m3_full_eval.py index 98af3c2e94..832c43f173 100755 --- a/utils/evals/minimax_m3_full_eval.py +++ b/utils/evals/minimax_m3_full_eval.py @@ -25,7 +25,7 @@ NATIVE_RESULTS_FILENAME = "minimax_vendor_results.jsonl" COMPATIBILITY_GLOB = "results_minimax_vendor_*.json" EXPECTED_RESULT_COUNT = 102 -UPSTREAM_REF = "85bf180e54e2ab0b31595cfdc697116c4760876d" +UPSTREAM_REF = "c899f95e17bfc4a338ddd4cb1638279125885e55" UPSTREAM_BASE_URL = ( "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/" f"{UPSTREAM_REF}" diff --git a/utils/evals/minimax_m3_smoke.json b/utils/evals/minimax_m3_smoke.json index 4ff3863a7d..b5a3eeb239 100644 --- a/utils/evals/minimax_m3_smoke.json +++ b/utils/evals/minimax_m3_smoke.json @@ -1,13 +1,12 @@ { - "source": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/85bf180e54e2ab0b31595cfdc697116c4760876d/sample.jsonl", - "ref": "85bf180e54e2ab0b31595cfdc697116c4760876d", + "source": "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/c899f95e17bfc4a338ddd4cb1638279125885e55/sample.jsonl", + "ref": "c899f95e17bfc4a338ddd4cb1638279125885e55", "indices": [ 71 ], "license": "MIT License\n\nCopyright (c) 2025 MiniMax Provider Verifier Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", "rows": [ { - "data_index": 71, "messages": [ { "role": "system", diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 33a2e87bae..43c102bd42 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -32,7 +32,7 @@ "aa7cec386fcb5e555aba0e8b1c31307940af41967708c9bc0f78b4e02e235dd5" ) EXPECTED_CASE_SHA256 = { - 71: "10272004ae08f4a7d08d2306404f6cbb7bbfa794230e1082a235ded036d550ed", + 71: "3d51571a1ed7d0bb644c3ae978ef5822b3150479b1e34bbbae7276f671657870", } UPSTREAM_SOURCE = ( "https://raw.githubusercontent.com/MiniMax-AI/MiniMax-Provider-Verifier/" @@ -71,8 +71,8 @@ def load_fixture(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: if not isinstance(raw_rows, list) or len(raw_rows) != 1: raise ValueError("fixture must contain exactly one row") row = dict(_mapping(raw_rows[0], "fixture.rows[0]")) - if row.get("data_index") != EXPECTED_INDICES[0]: - raise ValueError("fixture row must retain data_index 71") + if "data_index" in row: + raise ValueError("fixture request must not contain adapter-only data_index") digest = hashlib.sha256( json.dumps(row, ensure_ascii=False, separators=(",", ":")).encode() ).hexdigest() @@ -163,6 +163,12 @@ def _rate(value: Any, name: str) -> float: return float(value) +def _nonnegative_count(value: Any, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise SmokeSuiteError(f"native summary {name} must be a non-negative integer") + return value + + def _compatibility_path(output_dir: Path) -> Path: for stale_path in output_dir.glob(COMPATIBILITY_GLOB): stale_path.unlink() @@ -193,7 +199,8 @@ def _compatibility_result( "filter_list": [{"name": "strict-match"}], "native_metrics": [ "tool_calls_match_rate", - "tool_calls_schema_accuracy", + "tool_calls_schema_validation_error_count", + "tool_calls_total_count", "error_only_reasoning_rate", ], } @@ -230,8 +237,8 @@ def project_native_artifacts(*, output_dir: Path, model: str) -> Path: if len(result_lines) != 1 or not result_lines[0].strip(): raise SmokeSuiteError("native results must contain exactly one row") result = _mapping(json.loads(result_lines[0]), "native result") - if result.get("data_index") != EXPECTED_INDICES[0]: - raise SmokeSuiteError("native result must retain data_index 71") + if result.get("data_index") != 1: + raise SmokeSuiteError("native result must identify the one-row smoke input") if result.get("status") != "success": raise SmokeSuiteError("native verifier reported a request failure") if report.get("model") != model: @@ -240,9 +247,18 @@ def project_native_artifacts(*, output_dir: Path, model: str) -> Path: raise SmokeSuiteError("native summary does not describe one successful request") match_rate = _rate(report.get("tool_calls_match_rate"), "tool_calls_match_rate") - schema_rate = _rate( - report.get("tool_calls_schema_accuracy"), "tool_calls_schema_accuracy" + schema_errors = _nonnegative_count( + report.get("tool_calls_schema_validation_error_count"), + "tool_calls_schema_validation_error_count", ) + tool_call_total = _nonnegative_count( + report.get("tool_calls_total_count"), "tool_calls_total_count" + ) + if tool_call_total == 0 or schema_errors > tool_call_total: + raise SmokeSuiteError( + "native summary tool-call schema counts must describe one or more calls" + ) + schema_rate = 1.0 - (schema_errors / tool_call_total) reasoning_error_rate = _rate( report.get("error_only_reasoning_rate"), "error_only_reasoning_rate" ) diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 556c8ed8cd..9f86d9b79b 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -18,9 +18,11 @@ def _native_outputs( reasoning_error_rate: float = 0.0, ) -> None: (output_dir / mpe.NATIVE_RESULTS_FILENAME).write_text( - json.dumps({"data_index": 71, "status": status}) + "\n", + json.dumps({"data_index": 1, "status": status}) + "\n", encoding="utf-8", ) + tool_call_total = 10 + schema_errors = round((1.0 - schema_rate) * tool_call_total) (output_dir / mpe.NATIVE_REPORT_FILENAME).write_text( json.dumps( { @@ -28,7 +30,8 @@ def _native_outputs( "success_count": 1 if status == "success" else 0, "failure_count": 0 if status == "success" else 1, "tool_calls_match_rate": match_rate, - "tool_calls_schema_accuracy": schema_rate, + "tool_calls_schema_validation_error_count": schema_errors, + "tool_calls_total_count": tool_call_total, "error_only_reasoning_rate": reasoning_error_rate, } ) @@ -48,7 +51,8 @@ def test_fixture_is_exact_pinned_upstream_row() -> None: assert metadata["ref"] == mpe.UPSTREAM_REF assert metadata["indices"] == [71] - assert [row["data_index"] for row in rows] == [71] + assert set(rows[0]) == {"messages", "tools", "expected_tool_call"} + assert "data_index" not in rows[0] def test_prepare_smoke_input_preserves_fixture_row(tmp_path: Path) -> None: diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1b2c2ce253..cc9a7937e8 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -4,7 +4,6 @@ import io import json import os -import re import stat import subprocess import sys @@ -22,10 +21,6 @@ SINGLE_NODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-tmpl.yml" MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" -QWEN_SGLANG_MTP_LAUNCHERS = ( - REPO_ROOT / "benchmarks" / "single_node" / "agentic" / "qwen3.5_fp8_h100_mtp.sh", - REPO_ROOT / "benchmarks" / "single_node" / "agentic" / "qwen3.5_fp8_h200_mtp.sh", -) _SCRIPT = r""" source "$BENCHMARK_LIB" @@ -1327,14 +1322,14 @@ def test_kimi_vendor_multinode_runner_uses_fixed_upstream_contract( assert f"PYTHONPATH=<{tmp_path / 'runtime'}" in output assert ( "CHECKOUT=https://github.com/MoonshotAI/Kimi-Vendor-Verifier.git" - "@b9ed3a6665bdff2c943246f7d2903cd003d6ddd6" + "@3dad65a760a8867cda72f6dd8848d876a4e851b4" ) in output assert ( - "CHECKOUT_SHA=ab933117c894a785978f8aee0f052e5a9096b3029e7962354b1c07ea430588c3" + "CHECKOUT_SHA=ede9ea300c72ccfde9d8975ea4b1b54e423c7625690f6631ab1e65a715821e01" in output ) assert "PYTHON_ARG=<->" in output - assert "PYTHON_ARG=" in output + assert "PYTHON_ARG=<3dad65a760a8867cda72f6dd8848d876a4e851b4>" in output for value in ( adapter, verifier_dir, @@ -2547,11 +2542,6 @@ def test_eval_limit_full_and_zero_accepted(tmp_path): assert "--slice" not in argv -def test_qwen_sglang_launchers_expose_structured_tool_calls() -> None: - for launcher in QWEN_SGLANG_MTP_LAUNCHERS: - command = launcher.read_text() - assert "--reasoning-parser qwen3" in command - assert "--tool-call-parser qwen3_coder" in command def test_chat_route_readiness_requires_model_and_active_route(tmp_path: Path) -> None: @@ -2632,7 +2622,7 @@ def test_chat_route_readiness_accepts_stable_server_with_different_model_id( ) -def test_multinode_agentic_waits_for_openai_endpoint_before_requests( +def test_multinode_agentic_waits_only_for_eval_openai_endpoint( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" @@ -2652,33 +2642,34 @@ def test_multinode_agentic_waits_for_openai_endpoint_before_requests( encoding="utf-8", ) - subprocess.run( - ["bash", str(MULTINODE_AGENTIC_SCRIPT)], - env={ - **os.environ, - "INFMAX_CONTAINER_WORKSPACE": str(workspace), - "EVENTS": str(events_path), - "MODEL": "test-model", - "MODEL_PREFIX": "test-prefix", - "FRAMEWORK": "dynamo-vllm", - "PRECISION": "fp4", - "CONC": "1", - "RESULT_FILENAME": "result", - "RESULT_DIR": str(tmp_path / "results"), - "DURATION": "1", - }, - text=True, - capture_output=True, - check=True, - ) + base_env = { + **os.environ, + "INFMAX_CONTAINER_WORKSPACE": str(workspace), + "EVENTS": str(events_path), + "MODEL": "test-model", + "MODEL_PREFIX": "test-prefix", + "FRAMEWORK": "dynamo-vllm", + "PRECISION": "fp4", + "CONC": "1", + "RESULT_FILENAME": "result", + "RESULT_DIR": str(tmp_path / "results"), + "DURATION": "1", + } + expected_without_readiness = ["resolve", "deps", "build", "replay"] - assert events_path.read_text().splitlines() == [ - "resolve", - "deps", - "ready --port 8765", - "build", - "replay", - ] + for eval_only, expected in ( + ("false", expected_without_readiness), + ("true", [*expected_without_readiness[:2], "ready --port 8765", *expected_without_readiness[2:]]), + ): + events_path.unlink(missing_ok=True) + subprocess.run( + ["bash", str(MULTINODE_AGENTIC_SCRIPT)], + env={**base_env, "EVAL_ONLY": eval_only}, + text=True, + capture_output=True, + check=True, + ) + assert events_path.read_text().splitlines() == expected def test_agentic_eval_workflow_forwards_runner_contract() -> None: From 38dd3824218d80e73afe02d4a9da8dfc764eb1bd Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:53:10 -0500 Subject: [PATCH 88/99] refactor: narrow tool parser evaluation scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将工具解析器和服务就绪改动限制在 Kimi、MiniMax 及 eval-only 路径 --- .github/workflows/e2e-tests.yml | 9 ++-- benchmarks/multi_node/agentic_srt.sh | 4 +- .../agentic/agg-gb300-tp4-mtp-lowlatency.yaml | 5 --- .../agentic/agg-gb300-tp8-mtp-lowlatency.yaml | 5 --- .../agentic/agg-h200-tp8-mtp-kvoffload.yaml | 8 ---- ...-10p4d-dep8-dep16-c1536-mtp-kvoffload.yaml | 7 --- ...gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml | 7 --- ...00-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml | 7 --- ...00-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml | 7 --- ...00-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml | 7 --- .../disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml | 5 --- .../agentic/agg-gb200-tp2ep2-mtp.yaml | 2 - .../agentic/agg-gb200-tp4-mtp-hicache.yaml | 2 - .../gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml | 2 - ...g-gb300-tp2-c1-mtp-hicache-jid2530006.yaml | 2 - ...-gb300-tp2-c24-mtp-hicache-jid2530012.yaml | 2 - ...-gb300-tp2-c32-mtp-hicache-jid2530013.yaml | 2 - ...-gb300-tp2-c40-mtp-hicache-jid2530015.yaml | 2 - ...-gb300-tp2-c48-mtp-hicache-jid2530017.yaml | 2 - ...-gb300-tp2-c52-mtp-hicache-jid2527406.yaml | 2 - ...-gb300-tp2-c64-mtp-hicache-jid2527410.yaml | 2 - ...p2-c72-mtp-hicache-session-jid2527415.yaml | 7 --- ...4-c128-mtp-hicache-session-jid2527417.yaml | 7 --- ...p4-c16-mtp-hicache-session-jid2530027.yaml | 7 --- ...p4-c32-mtp-hicache-session-jid2530028.yaml | 7 --- ...p4-c64-mtp-hicache-session-jid2530029.yaml | 7 --- ...tp4-c8-mtp-hicache-session-jid2530030.yaml | 7 --- ...p4-c96-mtp-hicache-session-jid2527409.yaml | 7 --- .../agentic/agg-gb200-tp8-mtp-agentic.yaml | 6 --- .../agentic/glm5.2_fp4_mi355x_sglang_mtp.sh | 1 + .../agentic/qwen3.5_fp4_b200_sglang_mtp.sh | 3 ++ .../agentic/qwen3.5_fp4_b300_sglang_mtp.sh | 3 ++ .../agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh | 1 + .../agentic/qwen3.5_fp8_b200_sglang_mtp.sh | 3 ++ .../agentic/qwen3.5_fp8_b300_sglang_mtp.sh | 3 ++ .../agentic/qwen3.5_fp8_h100_mtp.sh | 2 - .../agentic/qwen3.5_fp8_h200_mtp.sh | 2 - .../agentic/qwen3.5_fp8_mi300x_mtp.sh | 1 + .../agentic/qwen3.5_fp8_mi325x_mtp.sh | 1 + perf-changelog.yaml | 12 +++++ runners/launch_b200-nscale-compat.sh | 6 ++- runners/launch_b300-nv.sh | 6 ++- runners/launch_gb200-nv.sh | 10 +++-- runners/launch_gb300-nv.sh | 6 +-- runners/launch_h100-dgxc-slurm.sh | 6 ++- runners/launch_h200-dgxc-slurm.sh | 6 ++- runners/patch_srt_eval_dispatch.py | 1 - runners/patch_srt_vllm_dp_ranks.py | 1 - runners/patch_trtllm_chat_store.py | 1 - runners/synthetic_injectors/sglang.py | 9 ++-- runners/test_slurm_utils.py | 44 +++---------------- utils/collect_eval_results.py | 3 +- utils/evals/test_bfcl_eval.py | 2 +- utils/evals/validate_scores.py | 1 + .../test_generate_sweep_configs.py | 16 ++++--- .../test_validate_reusable_sweep_artifacts.py | 2 +- utils/validate_reusable_sweep_artifacts.py | 2 +- 57 files changed, 88 insertions(+), 212 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4bf7781f65..dfd66b1037 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -263,18 +263,19 @@ jobs: echo "generate-cli-command is required outside trusted changelog dispatch mode" >&2 exit 1 fi + read -r -a GENERATE_ARGS <<< "$GENERATE_COMMAND" if [ "$TRIM_CONC" = "true" ]; then - GENERATE_COMMAND+=" --trim-conc" + GENERATE_ARGS+=(--trim-conc) fi if [ "$ALL_EVALS" = "true" ]; then - GENERATE_COMMAND+=" --all-evals" + GENERATE_ARGS+=(--all-evals) fi if [ "$EVALS_ONLY" = "true" ]; then - GENERATE_COMMAND+=" --evals-only" + GENERATE_ARGS+=(--evals-only) fi CONFIG_JSON=$(uv run --no-project --with pydantic --with pyyaml --python 3.12 \ "${GITHUB_WORKSPACE}/utils/matrix_logic/generate_sweep_configs.py" \ - $GENERATE_COMMAND) + "${GENERATE_ARGS[@]}") fi score_matrix() { local family="$1" diff --git a/benchmarks/multi_node/agentic_srt.sh b/benchmarks/multi_node/agentic_srt.sh index f88d3f5b71..e62123a99a 100644 --- a/benchmarks/multi_node/agentic_srt.sh +++ b/benchmarks/multi_node/agentic_srt.sh @@ -42,7 +42,9 @@ done resolve_trace_source install_agentic_deps -_wait_for_openai_chat_route --port "$PORT" +if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + _wait_for_openai_chat_route --port "$PORT" +fi wait_for_agentic_servers_idle() { local timeout_seconds="${AIPERF_DRAIN_TIMEOUT_SECONDS:-1800}" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-lowlatency.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-lowlatency.yaml index 6efda428ba..e67fade66a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-lowlatency.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp4-mtp-lowlatency.yaml @@ -44,9 +44,6 @@ frontend: PIP_BREAK_SYSTEM_PACKAGES: "1" DYN_NATS_REQUEST_TIMEOUT_SECS: "1800" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -73,8 +70,6 @@ backend: served-model-name: "deepseek-ai/DeepSeek-V4-Pro" enable-metrics: true trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 stream-interval: 10 watchdog-timeout: 1000000 mem-fraction-static: 0.94 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp8-mtp-lowlatency.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp8-mtp-lowlatency.yaml index b06a9b8e48..3f347d876f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp8-mtp-lowlatency.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-gb300-tp8-mtp-lowlatency.yaml @@ -44,9 +44,6 @@ frontend: PIP_BREAK_SYSTEM_PACKAGES: "1" DYN_NATS_REQUEST_TIMEOUT_SECS: "1800" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -73,8 +70,6 @@ backend: served-model-name: "deepseek-ai/DeepSeek-V4-Pro" enable-metrics: true trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 stream-interval: 10 watchdog-timeout: 1000000 mem-fraction-static: 0.94 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml index fd1f8c666b..fb12502bd6 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/agg-h200-tp8-mtp-kvoffload.yaml @@ -41,12 +41,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: - # Dynamo's default Rust chat processor does not consume SGLang's - # deepseekv4 parser setting. Parse DSML tool calls at the frontend so - # non-stream OpenAI responses expose message.tool_calls. - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -78,8 +72,6 @@ backend: served-model-name: "deepseek-ai/DeepSeek-V4-Pro" enable-metrics: true trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 stream-interval: 50 watchdog-timeout: 1000000 mem-fraction-static: 0.88 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-10p4d-dep8-dep16-c1536-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-10p4d-dep8-dep16-c1536-mtp-kvoffload.yaml index 7154a4b040..d0f771ff98 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-10p4d-dep8-dep16-c1536-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-10p4d-dep8-dep16-c1536-mtp-kvoffload.yaml @@ -55,9 +55,6 @@ frontend: DYN_TCP_REQUEST_TIMEOUT: "60" PIP_BREAK_SYSTEM_PACKAGES: "1" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -139,8 +136,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -176,8 +171,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml index 52775e8807..1365e9ed0b 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p1d-dep8-tp4-c80-mtp-kvoffload.yaml @@ -52,9 +52,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -139,8 +136,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -176,8 +171,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 4 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml index 49cfb2476a..c169da28c0 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-2p4d-dep8-dep16-c256-mtp-kvoffload.yaml @@ -52,9 +52,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -136,8 +133,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -173,8 +168,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml index 7a8a3b6f0e..f6c4e40a1f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-4p4d-dep8-dep16-c512-mtp-kvoffload.yaml @@ -51,9 +51,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -136,8 +133,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -173,8 +168,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml index 56f69a9675..d939becf8a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic/disagg-gb300-6p4d-dep8-dep16-c768-mtp-kvoffload.yaml @@ -52,9 +52,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: "1" args: - dyn-chat-processor: sglang - tool-call-parser: deepseekv4 - reasoning-parser: deepseek-v4 router-mode: "kv" router-session-affinity-ttl-secs: "3600" active-decode-blocks-threshold: "None" @@ -136,8 +133,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 8 @@ -173,8 +168,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: deepseek-v4 - tool-call-parser: deepseekv4 watchdog-timeout: 86400 stream-interval: 60 tp-size: 16 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml index a25a47a3c9..4d5a3696a5 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/glm5.2/agentic/disagg-h200-2p2d-pcp8-tp8-dp8-mtp.yaml @@ -39,11 +39,6 @@ frontend: DYN_ROUTER_TEMPERATURE: "10000000" PIP_BREAK_SYSTEM_PACKAGES: "1" args: - # Parse GLM tool tags at the Dynamo frontend so non-stream OpenAI - # responses expose message.tool_calls instead of raw tagged content. - dyn-chat-processor: sglang - tool-call-parser: glm47 - reasoning-parser: glm45 router-mode: "kv" router-reset-states: true active-decode-blocks-threshold: "None" diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml index a409d48f72..b4eb82c83a 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp2ep2-mtp.yaml @@ -29,8 +29,6 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 2 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml index 44b4819990..917f7e5765 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp-hicache.yaml @@ -29,8 +29,6 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml index 47ef58330a..6cbe8eccc3 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb200-fp4/agentic/agg-gb200-tp4-mtp.yaml @@ -41,8 +41,6 @@ backend: served-model-name: nvidia/Qwen3.5-397B-A17B-NVFP4-V2 model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml index dec6d21567..1ff01cb879 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c1-mtp-hicache-jid2530006.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml index b64d28c87e..a3e1e47e50 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c24-mtp-hicache-jid2530012.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml index 71e12602c1..77324cb75d 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c32-mtp-hicache-jid2530013.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml index 37a8413d7c..aca9321b4c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c40-mtp-hicache-jid2530015.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml index 9952dfcbb8..9aed4b026f 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c48-mtp-hicache-jid2530017.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml index 2f1580db28..17c2095906 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c52-mtp-hicache-jid2527406.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml index dafc4a9a87..d5a14ddf4d 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/agg-gb300-tp2-c64-mtp-hicache-jid2527410.yaml @@ -35,8 +35,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml index 105594d9cc..626f1e21b3 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp2-tp2-c72-mtp-hicache-session-jid2527415.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 2 @@ -161,8 +156,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 2 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml index c331e02c17..bf0b6dd7e2 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c128-mtp-hicache-session-jid2527417.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml index ddb0a449fa..1657ffb134 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c16-mtp-hicache-session-jid2530027.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml index 96bb5bd94a..67bfe4f61c 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c32-mtp-hicache-session-jid2530028.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml index 9ca1be72ad..14012bd254 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c64-mtp-hicache-session-jid2530029.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml index 594728bf54..f33326871d 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c8-mtp-hicache-session-jid2530030.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml index 5be3b9f497..0584b59017 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic/disagg-gb300-1p1d-tp4-tp4-c96-mtp-hicache-session-jid2527409.yaml @@ -31,9 +31,6 @@ frontend: env: PIP_BREAK_SYSTEM_PACKAGES: '1' args: - dyn-chat-processor: sglang - tool-call-parser: qwen3_coder - reasoning-parser: qwen3 router-mode: kv router-session-affinity-ttl-secs: '3600' active-decode-blocks-threshold: None @@ -114,8 +111,6 @@ backend: enable-metrics: true model-path: /model/ trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 tensor-parallel-size: 4 @@ -156,8 +151,6 @@ backend: quantization: modelopt_fp4 kv-cache-dtype: fp8_e4m3 trust-remote-code: true - reasoning-parser: qwen3 - tool-call-parser: qwen3_coder tensor-parallel-size: 4 data-parallel-size: 1 expert-parallel-size: 1 diff --git a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml index 6dd439f98a..f35d7c6bc8 100644 --- a/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml +++ b/benchmarks/multi_node/srt-slurm-recipes/vllm/deepseek-v4/agentic/agg-gb200-tp8-mtp-agentic.yaml @@ -44,12 +44,6 @@ frontend: type: dynamo enable_multiple_frontends: false args: - # Parse DeepSeek V4 tool tags at the Dynamo frontend so non-streaming - # OpenAI responses expose message.tool_calls instead of raw tagged content. - dyn-chat-processor: vllm - tool-call-parser: deepseek_v4 - reasoning-parser: deepseek_v4 - enable-auto-tool-choice: true router-mode: kv router-reset-states: true router-session-affinity-ttl-secs: 14400 diff --git a/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh b/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh index 81f1b66d42..98b4531e4b 100644 --- a/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/glm5.2_fp4_mi355x_sglang_mtp.sh @@ -4,6 +4,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" + export EVAL_FRAMEWORK="lm-eval" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh index fc956e14a0..f0fc1d09da 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh @@ -8,6 +8,9 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. +export EVAL_FRAMEWORK="lm-eval" + check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh index b43eb06454..a6a0ccf3bd 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_b300_sglang_mtp.sh @@ -8,6 +8,9 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. +export EVAL_FRAMEWORK="lm-eval" + check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh index 23f5136d1c..2cdcaa8cfa 100644 --- a/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_mi355x_sglang_mtp.sh @@ -8,6 +8,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +export EVAL_FRAMEWORK="lm-eval" check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh index 536602459b..472b9e4b12 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_b200_sglang_mtp.sh @@ -8,6 +8,9 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. +export EVAL_FRAMEWORK="lm-eval" + check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh index 96583ebe82..2d0a390250 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_b300_sglang_mtp.sh @@ -8,6 +8,9 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +# Use the lightweight GSM8K eval instead of the AgentX SWE-bench default. +export EVAL_FRAMEWORK="lm-eval" + check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ TOTAL_CPU_DRAM_GB RESULT_DIR DURATION diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh index 8f1e9d1730..41527c775d 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_h100_mtp.sh @@ -160,8 +160,6 @@ SGLANG_CMD=( --scheduler-recv-interval "$SCHEDULER_RECV_INTERVAL" --tokenizer-worker-num 6 --tokenizer-path "$MODEL" - --reasoning-parser qwen3 - --tool-call-parser qwen3_coder --enable-metrics "${SPEC_ARGS[@]}" "${CACHE_ARGS[@]}" diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh index 4d610faa04..518cd7e17e 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_h200_mtp.sh @@ -163,8 +163,6 @@ SGLANG_CMD=( --scheduler-recv-interval "$SCHEDULER_RECV_INTERVAL" --tokenizer-worker-num 6 --tokenizer-path "$MODEL" - --reasoning-parser qwen3 - --tool-call-parser qwen3_coder --enable-metrics "${SPEC_ARGS[@]}" "${CACHE_ARGS[@]}" diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh index faffe09721..aed9047466 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi300x_mtp.sh @@ -6,6 +6,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +export EVAL_FRAMEWORK="lm-eval" check_env_vars \ MODEL TP CONC EP_SIZE KV_OFFLOADING \ diff --git a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh index 5f4180dcaf..123e1c4acf 100755 --- a/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh +++ b/benchmarks/single_node/agentic/qwen3.5_fp8_mi325x_mtp.sh @@ -8,6 +8,7 @@ set -x source "$(dirname "$0")/../../benchmark_lib.sh" +export EVAL_FRAMEWORK="lm-eval" check_env_vars \ MODEL TP CONC EP_SIZE \ diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 5454cac345..186f885b5e 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -6674,3 +6674,15 @@ description: - "Split heterogeneous vLLM layer-compact KV backing storage into per-layer offload regions so MiniMax-M3 cache layouts initialize without invalid tensor views." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 + +- config-keys: + - kimik3-fp4-b300-vllm-agentic-dspark + - minimaxm3-fp4-gb200-dynamo-vllm-agentic-agg-mtp + - minimaxm3-fp4-gb200-dynamo-vllm-agentic-disagg-mtp + - minimaxm3-fp4-mi355x-vllm-agentic-mtp + scenario-type: + - agentic-coding + description: + - "Configure model-specific tool and reasoning parsers for the Kimi-K3 and MiniMax-M3 serving paths so opt-in verifier requests produce structured OpenAI tool calls." + - "Limit shared AgentX readiness and synthetic-acceptance rewrites to eval-only runs so default throughput recipe rendering remains unchanged." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2634 diff --git a/runners/launch_b200-nscale-compat.sh b/runners/launch_b200-nscale-compat.sh index b293d593c8..a93f0e5ccb 100644 --- a/runners/launch_b200-nscale-compat.sh +++ b/runners/launch_b200-nscale-compat.sh @@ -394,8 +394,10 @@ EOF # so large-model loads (e.g. DSR1-FP8 ~680GB off shared FS) finish in time. # Uses ${CONFIG_FILE%%:*} because CONFIG_FILE may carry an :override[N] suffix. sed -i 's/^ max_attempts: [0-9]*/ max_attempts: 720/' "${CONFIG_FILE%%:*}" - python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 + if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 + fi SRTCTL_PREFLIGHT_ARGS=() # Kimi K2.6 weights are staged on the Slurm compute nodes, not the login node. diff --git a/runners/launch_b300-nv.sh b/runners/launch_b300-nv.sh index c09121b2a9..b56c03a56b 100644 --- a/runners/launch_b300-nv.sh +++ b/runners/launch_b300-nv.sh @@ -271,8 +271,10 @@ sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" if [[ "$MODEL_PREFIX" == "minimaxm3" && -n "$MINIMAX_M3_SLURM_EXCLUDED_NODELIST" ]]; then sed -i "/^name:.*/a sbatch_directives:\n exclude: \"${MINIMAX_M3_SLURM_EXCLUDED_NODELIST}\"" "$CONFIG_PATH" fi -python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "$CONFIG_PATH" "$FRAMEWORK" || exit 1 +if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 +fi SRTCTL_APPLY_ARGS=( -f "$CONFIG_FILE" --tags "b300,${MODEL_PREFIX},${PRECISION},${ISL}x${OSL},infmax-$(date +%Y%m%d)" diff --git a/runners/launch_gb200-nv.sh b/runners/launch_gb200-nv.sh index 0bc3413fda..0cdb570d3e 100755 --- a/runners/launch_gb200-nv.sh +++ b/runners/launch_gb200-nv.sh @@ -711,10 +711,12 @@ if command -v squeue >/dev/null 2>&1; then fi sed -i "s/^name:.*/name: \"${SRT_SLURM_JOB_NAME}\"/" "$CONFIG_PATH" -# Restore real acceptance for eval-only jobs, or inject synthetic acceptance -# when a throughput run explicitly enables it. -python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "$CONFIG_PATH" "$FRAMEWORK" || exit 1 +# Restore real acceptance only for eval jobs. Throughput recipe rendering remains +# unchanged by the opt-in evaluator path. +if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 +fi # Don't leak the login-node venv to the compute-node orchestrator. sbatch's # default --export=ALL propagates VIRTUAL_ENV (set by `source diff --git a/runners/launch_gb300-nv.sh b/runners/launch_gb300-nv.sh index f1d9b839ac..91fb3b4918 100644 --- a/runners/launch_gb300-nv.sh +++ b/runners/launch_gb300-nv.sh @@ -524,12 +524,10 @@ fi # below still receives the full CONFIG_FILE (with selector). CONFIG_PATH="${CONFIG_FILE%%:*}" sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" -python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "$CONFIG_PATH" "$FRAMEWORK" || exit 1 # Throughput recipes opt into synthetic acceptance through the master config. -# Eval-only jobs leave the checked-in real-MTP recipe unchanged so generated -# tokens still pass target-model verification. +# Eval-only jobs remove those settings so generated tokens use real target-model +# verification. inject_synthetic_acceptance "$CONFIG_PATH" "$FRAMEWORK" || exit 1 # --no-preflight skips srtctl's pre-submit model-path stat, which runs on diff --git a/runners/launch_h100-dgxc-slurm.sh b/runners/launch_h100-dgxc-slurm.sh index 20599b8bfb..50c6a8ad1f 100644 --- a/runners/launch_h100-dgxc-slurm.sh +++ b/runners/launch_h100-dgxc-slurm.sh @@ -150,8 +150,10 @@ EOF sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_FILE" # Raise sglang's torch-distributed TCPStore timeout from the 600s gloo default sed -i '/^ watchdog-timeout:/a\ dist-timeout: 1800' "${CONFIG_FILE%%:*}" - python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 + if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "${CONFIG_FILE%%:*}" "$FRAMEWORK" || exit 1 + fi SRTCTL_OUTPUT=$(srtctl apply -f "$CONFIG_FILE" --tags "h100,${MODEL_PREFIX},${PRECISION},${ISL}x${OSL},infmax-$(date +%Y%m%d)" 2>&1) echo "$SRTCTL_OUTPUT" diff --git a/runners/launch_h200-dgxc-slurm.sh b/runners/launch_h200-dgxc-slurm.sh index 00278ab820..8e3d8235b1 100755 --- a/runners/launch_h200-dgxc-slurm.sh +++ b/runners/launch_h200-dgxc-slurm.sh @@ -317,8 +317,10 @@ EOF sed -i "s/^name:.*/name: \"${RUNNER_NAME}\"/" "$CONFIG_PATH" sed -i '/^health_check:/,/^[^ ]/{ /^health_check:/d; /^ /d; }' "$CONFIG_PATH" printf '\nhealth_check:\n max_attempts: 720\n interval_seconds: 10\n' >> "$CONFIG_PATH" - python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "$CONFIG_PATH" "$FRAMEWORK" + if [[ "${EVAL_ONLY:-false}" == "true" ]]; then + python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 + fi WORKLOAD_TAG="${ISL}x${OSL}" if [[ "$IS_AGENTIC" == "1" ]]; then WORKLOAD_TAG="agentic" diff --git a/runners/patch_srt_eval_dispatch.py b/runners/patch_srt_eval_dispatch.py index 5a906978b6..206e9c0133 100755 --- a/runners/patch_srt_eval_dispatch.py +++ b/runners/patch_srt_eval_dispatch.py @@ -6,7 +6,6 @@ import sys from pathlib import Path - DO_SWEEP_ENV_BLOCK = """ "EVAL_ONLY", "IS_MULTINODE",""" DO_SWEEP_ENV_REPLACEMENT = """ "EVAL_ONLY", diff --git a/runners/patch_srt_vllm_dp_ranks.py b/runners/patch_srt_vllm_dp_ranks.py index dbc11da40b..9decf0f06e 100755 --- a/runners/patch_srt_vllm_dp_ranks.py +++ b/runners/patch_srt_vllm_dp_ranks.py @@ -6,7 +6,6 @@ import sys from pathlib import Path - OLD_BLOCK = ''' else: # DP+EP mode: one process per GPU # Each process gets a single GPU and a unique dp_rank diff --git a/runners/patch_trtllm_chat_store.py b/runners/patch_trtllm_chat_store.py index 613f368560..09fa4e5160 100755 --- a/runners/patch_trtllm_chat_store.py +++ b/runners/patch_trtllm_chat_store.py @@ -7,7 +7,6 @@ import sys from pathlib import Path - CLASS_HEADER = "class ChatCompletionRequest(OpenAIBaseModel):\n" NEXT_CLASS = "\nclass " FIELD_ANCHOR = " stream: Optional[bool] = False\n" diff --git a/runners/synthetic_injectors/sglang.py b/runners/synthetic_injectors/sglang.py index ed58144776..d5cd6e0b16 100644 --- a/runners/synthetic_injectors/sglang.py +++ b/runners/synthetic_injectors/sglang.py @@ -19,10 +19,9 @@ def spec_tokens_from_recipe(text): def rewrite(content, al, log): - """Set throughput-only golden acceptance in each worker role.""" - content, removed = _SIMULATED_ACCEPTANCE_ENV_RE.subn("", content) - if removed: - log(f"Replaced {removed} existing SGLANG_SIMULATE_ACC_* variable(s)") + """Add throughput-only golden-acceptance variables to each worker role.""" + if "SGLANG_SIMULATE_ACC_LEN" in content: + raise ValueError("recipe already contains SGLANG_SIMULATE_ACC_* variables") variables = ( f'\n SGLANG_SIMULATE_ACC_LEN: "{al:g}"' @@ -34,7 +33,7 @@ def rewrite(content, al, log): content, ) if count: - log(f"Set SGLANG_SIMULATE_ACC_* in {count} worker environment block(s)") + log(f"Added SGLANG_SIMULATE_ACC_* to {count} worker environment block(s)") return rewritten, count diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index 1aa6506151..cfd80439e1 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -396,11 +396,11 @@ def test_eval_only_removes_sglang_simulated_acceptance(tmp_path: Path) -> None: assert "KEEP_ME: unchanged" in rewritten -def test_sglang_throughput_replaces_existing_simulated_acceptance( +def test_sglang_throughput_rejects_existing_simulated_acceptance( tmp_path: Path, ) -> None: recipe = tmp_path / "recipe.yaml" - recipe.write_text( + original = ( "backend:\n" " aggregated_environment:\n" ' SGLANG_SIMULATE_ACC_LEN: "2.99"\n' @@ -408,6 +408,7 @@ def test_sglang_throughput_replaces_existing_simulated_acceptance( ' SGLANG_SIMULATE_ACC_TOKEN_MODE: "real-draft-token"\n' " KEEP_ME: unchanged\n" ) + recipe.write_text(original) result = subprocess.run( ["python3", str(INJECT_ACCEPTANCE), str(recipe), "dynamo-sglang"], @@ -421,11 +422,10 @@ def test_sglang_throughput_replaces_existing_simulated_acceptance( text=True, ) - assert result.returncode == 0, result.stderr - rewritten = recipe.read_text() - assert rewritten.count("SGLANG_SIMULATE_ACC_LEN") == 1 - assert 'SGLANG_SIMULATE_ACC_LEN: "3.39"' in rewritten - assert "KEEP_ME: unchanged" in rewritten + assert result.returncode != 0 + assert "already contains SGLANG_SIMULATE_ACC_" in result.stderr + assert recipe.read_text() == original + def test_eval_only_acceptance_rewrite_allows_non_speculative_recipe( tmp_path: Path, @@ -545,36 +545,6 @@ def test_mi355_minimax_launcher_configures_reasoning_parser() -> None: assert "--enable-auto-tool-choice" in launcher -def test_dynamo_sglang_agentic_recipes_parse_tools_at_frontend() -> None: - recipe_roots = ( - ( - REPO_ROOT - / "benchmarks/multi_node/srt-slurm-recipes/sglang/deepseek-v4/agentic", - ("deepseekv4", "deepseek-v4"), - ), - ( - REPO_ROOT - / "benchmarks/multi_node/srt-slurm-recipes/sglang/qwen3.5/gb300-fp4/agentic", - ("qwen3_coder", "qwen3"), - ), - ) - checked = 0 - - for recipe_root, (tool_parser, reasoning_parser) in recipe_roots: - for recipe_path in recipe_root.glob("*.yaml"): - recipe = yaml.safe_load(recipe_path.read_text()) - frontend = recipe["frontend"] - if frontend["type"] != "dynamo": - continue - args = frontend["args"] - assert args["dyn-chat-processor"] == "sglang", recipe_path - assert args["tool-call-parser"] == tool_parser, recipe_path - assert args["reasoning-parser"] == reasoning_parser, recipe_path - checked += 1 - - assert checked == 15 - - def test_swebench_container_paths_forward_modal_credentials() -> None: paths = ( REPO_ROOT / "benchmarks/multi_node/llm-d/submit.sh", diff --git a/utils/collect_eval_results.py b/utils/collect_eval_results.py index 567f877260..7792a983c1 100644 --- a/utils/collect_eval_results.py +++ b/utils/collect_eval_results.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -import sys import json import math import re +import sys from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, List, Optional, Tuple + from tabulate import tabulate MODEL = "Model" diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index 359a2b2130..f3f4246e9e 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -3,8 +3,8 @@ import os import subprocess import sys -from types import ModuleType from pathlib import Path +from types import ModuleType from typing import Any import pytest diff --git a/utils/evals/validate_scores.py b/utils/evals/validate_scores.py index 9450892f55..ebea50fb5e 100644 --- a/utils/evals/validate_scores.py +++ b/utils/evals/validate_scores.py @@ -2,6 +2,7 @@ """Validate eval scores against per-task and per-model thresholds.""" from __future__ import annotations + import argparse import glob import json diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index d30f554194..44b7693438 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -1878,7 +1878,7 @@ class TestArgumentDefaults: def test_runner_config_default_value(self): """Verify --runner-config defaults to configs/runners.yaml.""" import sys - from generate_sweep_configs import main + # Save original sys.argv original_argv = sys.argv @@ -1895,7 +1895,7 @@ def test_runner_config_default_value(self): # Parse args using the ArgumentParser from main # We need to access the parser directly import argparse - from generate_sweep_configs import main + # Create the same parent parser as in main() parent_parser = argparse.ArgumentParser(add_help=False) @@ -2002,6 +2002,7 @@ def test_all_evals_cli_marks_every_fixed_sequence_entry( """--all-evals bypasses the default min-conc/highest-median policy but still only evaluates 8k1k (1k1k entries are excluded).""" import sys + import generate_sweep_configs monkeypatch.setattr( @@ -2041,6 +2042,7 @@ def test_all_evals_composes_with_evals_only( sample_runner_config, ): import sys + import generate_sweep_configs monkeypatch.setattr( @@ -2078,6 +2080,7 @@ def test_trim_conc_reduces_generated_eval_matrix( sample_runner_config, ): import sys + import generate_sweep_configs monkeypatch.setattr( @@ -2132,6 +2135,7 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( monkeypatch, ): import sys + import generate_sweep_configs repo_root = Path(__file__).resolve().parents[2] @@ -2187,10 +2191,10 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( json.dumps(manifest, separators=(',', ':')).encode() ).hexdigest() - assert len(manifest) == 67 + assert len(manifest) == 63 assert manifest_digest == ( - 'b94441edd3d083d3c03a6ed1f3891ee222c420614ba6a2a68802d9a85c128ca2' - ), json.dumps(manifest, indent=2) + '3e4a3195f921e87e97ccfe525b570ba9180ab0acc13206f591d687d9dbe18f14' + ), manifest_digest for row in rows: if isinstance(row['conc'], list): assert row['conc'] == [row['eval-conc']] @@ -2204,6 +2208,7 @@ def test_all_evals_batches_each_multinode_concurrency( sample_runner_config, ): import sys + import generate_sweep_configs config = sample_multinode_config @@ -2246,6 +2251,7 @@ def test_all_evals_batches_each_multinode_concurrency( def test_all_evals_cannot_combine_with_no_evals(self, monkeypatch): import sys + import generate_sweep_configs monkeypatch.setattr(sys, 'argv', [ diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index a43c6bd889..f66486a8f1 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -8,9 +8,9 @@ from validate_reusable_sweep_artifacts import ( agentic_key, benchmark_key, + dedupe_reran_evals, eval_key, eval_result_key, - dedupe_reran_evals, main, validate_agentic_artifacts, validate_eval_artifacts, diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 9c50bf8240..d83bbd8f3a 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -9,8 +9,8 @@ import re import shutil import sys -from datetime import datetime, timezone from collections import Counter +from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable, Optional From 1c0d6f2ac8f32da6a607a3b977c01a1d2275c75f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:53:19 -0500 Subject: [PATCH 89/99] docs: document runtime patch removal gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:记录 PR 2634 运行时补丁的适用范围、验证证据和移除条件 --- docs/waiver/2634.md | 56 ++++++++++++++++++++++++++++++++++++++++++ docs/waiver/2634_zh.md | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 docs/waiver/2634.md create mode 100644 docs/waiver/2634_zh.md diff --git a/docs/waiver/2634.md b/docs/waiver/2634.md new file mode 100644 index 0000000000..10041f79e3 --- /dev/null +++ b/docs/waiver/2634.md @@ -0,0 +1,56 @@ +English | [中文](2634_zh.md) + +# Inference-engine patch waiver for PR 2634 + +Filed under the inference-engine and serving-stack patch item in [`docs/PR_REVIEW_CHECKLIST.md`](../PR_REVIEW_CHECKLIST.md). PR 2634 applies four exact, fail-closed rewrites to pinned upstream installations before serving or evaluation. The rewrites are required for the opt-in Kimi K3 and MiniMax M3 tool-use evaluations described below. + +## Configurations covered + +| Patch | Configurations | Pinned upstream | +|---|---|---| +| vLLM SimpleCPUOffload layer regions | `minimaxm3-fp4-b200-vllm-agentic-mtp`, `minimaxm3-fp4-b300-vllm-agentic-mtp` | `vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45` | +| TensorRT-LLM `store=false` request field | `minimaxm3-fp4-b200-trtllm-agentic-mtp`, `minimaxm3-fp4-b300-trtllm-agentic-mtp` | `nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc23.post1` | +| srt-slurm external DP rank allocation | `kimik3-fp4-gb200-dynamo-vllm-agentic` | NVIDIA/srt-slurm `v1.0.53`, commit `217f94387abeddfed7149a71955dc523e07cd765` | +| srt-slurm provider-eval dispatch | Opt-in `kimi-vendor`, `minimax-vendor`, and `bfcl` eval-only rows launched through the NVIDIA srt-slurm paths | Each launcher's checked and commit-verified srt-slurm revision | + +Patch entrypoints are [`runners/patch_vllm_simple_kv_offload.py`](../../runners/patch_vllm_simple_kv_offload.py), [`runners/patch_trtllm_chat_store.py`](../../runners/patch_trtllm_chat_store.py), [`runners/patch_srt_vllm_dp_ranks.py`](../../runners/patch_srt_vllm_dp_ranks.py), and [`runners/patch_srt_eval_dispatch.py`](../../runners/patch_srt_eval_dispatch.py). + +## Exact changes and necessity + +### vLLM SimpleCPUOffload + +MiniMax M3 uses heterogeneous KDA and MLA KV layouts. The pinned vLLM worker treats the first logical allocation size as one common backing region and attempts to reshape every layer through that size. The resulting tensor view is invalid for layers with a different physical block stride. The patch detects heterogeneous storage, slices each layer from its real storage offset, validates the required region size, and keeps the original shared-storage behavior for homogeneous layouts. + +The stock image fails before serving offloaded MiniMax M3 rows. The patched path produced complete BFCL artifacts with score `1.0` on B200 in run [33328257980](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33328257980) and on B300 in run [33330971674](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33330971674). vLLM issue [41830](https://github.com/vllm-project/vllm/issues/41830) documents the related unsupported interaction between SimpleCPUOffload and heterogeneous or hybrid KV cache management. No released upstream fix matches this exact layer-region rewrite yet. + +### TensorRT-LLM chat request compatibility + +BFCL's stock OpenAI client sends the standard optional `store=false` field. TensorRT-LLM `1.3.0rc23.post1` rejects the request with HTTP 400 because its `ChatCompletionRequest` model does not declare the field. The patch adds only `store: Literal[False] = False`, so persistence requests remain rejected while the non-persistent standard request is accepted. Run [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) completed all four BFCL smoke requests through the patched B200 TensorRT-LLM endpoint. The resulting `0.25` score is model quality, not request compatibility. + +### srt-slurm external DP rank allocation + +srt-slurm `v1.0.53` creates one process per GPU for external DP, even when each DP rank owns `TP × PP` GPUs. Kimi K3 TP4 and DP4 therefore receives sixteen one-GPU ranks instead of four four-GPU ranks. The patch validates `DP × TP × PP` against the allocation and creates one process with the complete GPU set for each DP rank. GB200 run [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) produced complete BFCL artifacts with score `1.0` through that topology. + +NVIDIA/srt-slurm `v1.0.82` now contains a per-node external-DP implementation with combined data and model parallelism. PR 2634 retains the hardware-validated `v1.0.53` pin until the Kimi recipe is rendered and exercised against the newer release. + +### srt-slurm provider-eval dispatch + +The pinned srt-slurm revisions hardcode the `lm-eval` runner and forward only the legacy eval environment. The opt-in provider suites require `EVAL_FRAMEWORK`, `EVAL_SUITE`, `EVAL_LIMIT`, and their native artifact families. The patch extends only the eval environment whitelist and eval command. Throughput jobs do not activate it, and every replacement is exact-count and fail-closed. + +The latest srt-slurm `v1.0.82` still constructs the post-eval runner with `get_runner("lm-eval")`, so upgrading alone does not provide provider dispatch. + +## Safety controls + +- Every patch matches one exact pristine source block and fails on missing, duplicate, or partially patched state. +- Reapplying a complete patch is a no-op. +- Engine patches are restricted to the named MiniMax configurations. The eval-dispatch patch runs only when `EVAL_FRAMEWORK` is not `lm-eval`. +- Eval-only acceptance rewrites are gated by `EVAL_ONLY=true`; default throughput recipe rendering is unchanged. +- Unit tests cover pristine, idempotent, partial, and unsupported source states. + +## Removal plan + +1. Remove `patch_vllm_simple_kv_offload.py` when an official vLLM image correctly allocates heterogeneous SimpleCPUOffload layer regions. Re-run the B200 and B300 DRAM-offload rows against the stock image before deleting the patch and this waiver section. +2. Remove `patch_trtllm_chat_store.py` when the pinned TensorRT-LLM release accepts the standard non-persistent `store=false` field. Re-run BFCL smoke on B200 and B300 without the patch. +3. Migrate the Kimi GB200 recipes to srt-slurm `v1.0.82` or newer, verify rendered DP4 and TP4 commands and hardware artifacts, then delete `patch_srt_vllm_dp_ranks.py`. +4. Remove `patch_srt_eval_dispatch.py` when the pinned srt-slurm release supports selecting external eval runners and staging their native artifacts without a source rewrite. +5. Delete this waiver in the same PR that removes the final covered runtime patch. diff --git a/docs/waiver/2634_zh.md b/docs/waiver/2634_zh.md new file mode 100644 index 0000000000..a803be063b --- /dev/null +++ b/docs/waiver/2634_zh.md @@ -0,0 +1,56 @@ +[English](2634.md) | 中文 + +# PR 2634 推理引擎补丁豁免说明 + +本文档对应 [`docs/PR_REVIEW_CHECKLIST.md`](../PR_REVIEW_CHECKLIST.md) 中关于推理引擎与服务栈补丁的检查项。PR 2634 在启动服务或评估之前,对固定版本的上游安装执行四项精确、失败即终止的源码改写。这些改写用于下文所列的 Kimi K3 和 MiniMax M3 工具调用评估,且仅在显式启用时生效。 + +## 适用配置 + +| 补丁 | 配置 | 固定的上游版本 | +|---|---|---| +| vLLM SimpleCPUOffload 分层存储区域 | `minimaxm3-fp4-b200-vllm-agentic-mtp`、`minimaxm3-fp4-b300-vllm-agentic-mtp` | `vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45` | +| TensorRT-LLM `store=false` 请求字段 | `minimaxm3-fp4-b200-trtllm-agentic-mtp`、`minimaxm3-fp4-b300-trtllm-agentic-mtp` | `nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc23.post1` | +| srt-slurm 外部数据并行(DP) rank 分配 | `kimik3-fp4-gb200-dynamo-vllm-agentic` | NVIDIA/srt-slurm `v1.0.53`,commit `217f94387abeddfed7149a71955dc523e07cd765` | +| srt-slurm provider 评估调度 | 通过 NVIDIA srt-slurm 路径启动的 `kimi-vendor`、`minimax-vendor` 和 `bfcl` eval-only 行 | 各启动器经过校验且固定 commit 的 srt-slurm 版本 | + +补丁入口分别为 [`runners/patch_vllm_simple_kv_offload.py`](../../runners/patch_vllm_simple_kv_offload.py)、[`runners/patch_trtllm_chat_store.py`](../../runners/patch_trtllm_chat_store.py)、[`runners/patch_srt_vllm_dp_ranks.py`](../../runners/patch_srt_vllm_dp_ranks.py) 和 [`runners/patch_srt_eval_dispatch.py`](../../runners/patch_srt_eval_dispatch.py)。 + +## 具体改动与必要性 + +### vLLM SimpleCPUOffload + +MiniMax M3 使用异构的 KDA 与 MLA KV 布局。固定版本的 vLLM worker 把第一段逻辑分配大小视为统一的后备存储区域,并按该大小重排所有层。对于物理 block 步长不同的层,这会产生非法 tensor view。补丁会检测异构存储,按真实存储偏移切分各层,校验所需区域大小,同时保留同构布局原有的共享存储逻辑。 + +原始镜像会在 DRAM offload 的 MiniMax M3 配置开始服务前失败。应用补丁后,B200 的运行 [33328257980](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33328257980) 和 B300 的运行 [33330971674](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33330971674) 均生成了完整 BFCL 产物,得分为 `1.0`。vLLM issue [41830](https://github.com/vllm-project/vllm/issues/41830) 记录了 SimpleCPUOffload 与异构或混合 KV cache 管理之间的相关兼容性问题。目前尚无已发布的上游修复能替代这项分层存储区域改写。 + +### TensorRT-LLM chat 请求兼容性 + +BFCL 原生 OpenAI 客户端会发送标准可选字段 `store=false`。TensorRT-LLM `1.3.0rc23.post1` 的 `ChatCompletionRequest` 模型未声明该字段,因此以 HTTP 400 拒绝请求。补丁仅添加 `store: Literal[False] = False`,仍会拒绝要求持久化的请求,只接受标准的非持久化请求。运行 [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) 通过应用补丁的 B200 TensorRT-LLM endpoint 完成了全部四个 BFCL smoke 请求。最终 `0.25` 得分属于模型质量问题,而非请求兼容性问题。 + +### srt-slurm 外部 DP rank 分配 + +srt-slurm `v1.0.53` 在外部 DP 模式下为每张 GPU 创建一个进程,即使每个 DP rank 应占用 `TP × PP` 张 GPU。Kimi K3 的 TP4 与 DP4 因此会错误地得到十六个单 GPU rank,而不是四个四 GPU rank。补丁会校验 `DP × TP × PP` 与分配资源一致,并为每个 DP rank 创建一个持有完整 GPU 集合的进程。GB200 运行 [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) 在该拓扑上生成了完整 BFCL 产物,得分为 `1.0`。 + +NVIDIA/srt-slurm `v1.0.82` 已包含支持数据并行与模型并行组合的 per-node 外部 DP 实现。PR 2634 暂时保留经过硬件验证的 `v1.0.53` 固定版本,待 Kimi recipe 使用新版本完成渲染检查与硬件验证后迁移。 + +### srt-slurm provider 评估调度 + +固定的 srt-slurm 版本只支持硬编码的 `lm-eval` runner,并且仅传递旧版评估环境变量。显式启用的 provider 评估需要 `EVAL_FRAMEWORK`、`EVAL_SUITE`、`EVAL_LIMIT` 以及各自的原生产物。补丁只扩展评估环境变量白名单和评估命令。吞吐量任务不会触发这些逻辑,所有替换都要求精确匹配次数,否则立即失败。 + +最新 srt-slurm `v1.0.82` 仍使用 `get_runner("lm-eval")` 构建后置评估 runner,因此仅升级版本无法提供 provider 调度能力。 + +## 安全措施 + +- 每项补丁只匹配一段完全一致的原始源码。目标缺失、重复或处于部分补丁状态时都会失败。 +- 对已完整应用补丁的源码重复执行时不会产生改动。 +- 推理引擎补丁仅用于上述 MiniMax 配置。仅当 `EVAL_FRAMEWORK` 不为 `lm-eval` 时才应用评估调度补丁。 +- 评估专用的 acceptance 改写受 `EVAL_ONLY=true` 控制,默认吞吐量 recipe 渲染不变。 +- 单元测试覆盖原始、幂等、部分补丁和不受支持的源码状态。 + +## 移除计划 + +1. 当官方 vLLM 镜像能够正确分配异构 SimpleCPUOffload 分层存储区域时,使用原始镜像重新运行 B200 和 B300 DRAM offload 配置,验证通过后删除 `patch_vllm_simple_kv_offload.py` 及本豁免中的对应章节。 +2. 当固定版本的 TensorRT-LLM 支持标准非持久化字段 `store=false` 时,在不应用补丁的情况下重新运行 B200 和 B300 BFCL smoke,随后删除 `patch_trtllm_chat_store.py`。 +3. 将 Kimi GB200 recipe 升级至 srt-slurm `v1.0.82` 或更高版本,验证渲染出的 DP4 与 TP4 命令及硬件产物,随后删除 `patch_srt_vllm_dp_ranks.py`。 +4. 当固定版本的 srt-slurm 能够选择外部评估 runner,并在无需源码改写的情况下归档其原生产物时,删除 `patch_srt_eval_dispatch.py`。 +5. 在移除最后一项受本文件覆盖的运行时补丁时,同步删除本豁免文档。 From 03caa0080bca27911f4340e46e0a2b9238b2ca91 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:20:47 -0500 Subject: [PATCH 90/99] docs: link upstream runtime patch tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:补充运行时补丁对应的上游 PR 和 issue 链接 --- docs/waiver/2634.md | 6 +++--- docs/waiver/2634_zh.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/waiver/2634.md b/docs/waiver/2634.md index 10041f79e3..ddf25a4ebb 100644 --- a/docs/waiver/2634.md +++ b/docs/waiver/2634.md @@ -25,19 +25,19 @@ The stock image fails before serving offloaded MiniMax M3 rows. The patched path ### TensorRT-LLM chat request compatibility -BFCL's stock OpenAI client sends the standard optional `store=false` field. TensorRT-LLM `1.3.0rc23.post1` rejects the request with HTTP 400 because its `ChatCompletionRequest` model does not declare the field. The patch adds only `store: Literal[False] = False`, so persistence requests remain rejected while the non-persistent standard request is accepted. Run [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) completed all four BFCL smoke requests through the patched B200 TensorRT-LLM endpoint. The resulting `0.25` score is model quality, not request compatibility. +BFCL's stock OpenAI client sends the standard optional `store=false` field. TensorRT-LLM `1.3.0rc23.post1` rejects the request with HTTP 400 because its `ChatCompletionRequest` model does not declare the field. The patch adds only `store: Literal[False] = False`, so persistence requests remain rejected while the non-persistent standard request is accepted. Run [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) completed all four BFCL smoke requests through the patched B200 TensorRT-LLM endpoint. The resulting `0.25` score is model quality, not request compatibility. Upstream issue [9709](https://github.com/NVIDIA/TensorRT-LLM/issues/9709) tracks the same request-model extension mechanism for missing OpenAI-compatible fields; no dedicated upstream `store` issue exists yet. ### srt-slurm external DP rank allocation srt-slurm `v1.0.53` creates one process per GPU for external DP, even when each DP rank owns `TP × PP` GPUs. Kimi K3 TP4 and DP4 therefore receives sixteen one-GPU ranks instead of four four-GPU ranks. The patch validates `DP × TP × PP` against the allocation and creates one process with the complete GPU set for each DP rank. GB200 run [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) produced complete BFCL artifacts with score `1.0` through that topology. -NVIDIA/srt-slurm `v1.0.82` now contains a per-node external-DP implementation with combined data and model parallelism. PR 2634 retains the hardware-validated `v1.0.53` pin until the Kimi recipe is rendered and exercised against the newer release. +NVIDIA/srt-slurm [PR 331](https://github.com/NVIDIA/srt-slurm/pull/331), released in `v1.0.82`, added a per-node external-DP implementation with combined data and model parallelism. PR 2634 retains the hardware-validated `v1.0.53` pin until the Kimi recipe is rendered and exercised against the newer release. ### srt-slurm provider-eval dispatch The pinned srt-slurm revisions hardcode the `lm-eval` runner and forward only the legacy eval environment. The opt-in provider suites require `EVAL_FRAMEWORK`, `EVAL_SUITE`, `EVAL_LIMIT`, and their native artifact families. The patch extends only the eval environment whitelist and eval command. Throughput jobs do not activate it, and every replacement is exact-count and fail-closed. -The latest srt-slurm `v1.0.82` still constructs the post-eval runner with `get_runner("lm-eval")`, so upgrading alone does not provide provider dispatch. +The merged upstream lm-eval implementation came through NVIDIA/srt-slurm [PR 122](https://github.com/NVIDIA/srt-slurm/pull/122). The latest `v1.0.82` still constructs the post-eval runner with `get_runner("lm-eval")`, so upgrading alone does not provide provider dispatch. Open upstream [PR 41](https://github.com/NVIDIA/srt-slurm/pull/41) is the tracked generalization path for external or custom eval harnesses. ## Safety controls diff --git a/docs/waiver/2634_zh.md b/docs/waiver/2634_zh.md index a803be063b..0731d86b48 100644 --- a/docs/waiver/2634_zh.md +++ b/docs/waiver/2634_zh.md @@ -25,19 +25,19 @@ MiniMax M3 使用异构的 KDA 与 MLA KV 布局。固定版本的 vLLM worker ### TensorRT-LLM chat 请求兼容性 -BFCL 原生 OpenAI 客户端会发送标准可选字段 `store=false`。TensorRT-LLM `1.3.0rc23.post1` 的 `ChatCompletionRequest` 模型未声明该字段,因此以 HTTP 400 拒绝请求。补丁仅添加 `store: Literal[False] = False`,仍会拒绝要求持久化的请求,只接受标准的非持久化请求。运行 [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) 通过应用补丁的 B200 TensorRT-LLM endpoint 完成了全部四个 BFCL smoke 请求。最终 `0.25` 得分属于模型质量问题,而非请求兼容性问题。 +BFCL 原生 OpenAI 客户端会发送标准可选字段 `store=false`。TensorRT-LLM `1.3.0rc23.post1` 的 `ChatCompletionRequest` 模型未声明该字段,因此以 HTTP 400 拒绝请求。补丁仅添加 `store: Literal[False] = False`,仍会拒绝要求持久化的请求,只接受标准的非持久化请求。运行 [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) 通过应用补丁的 B200 TensorRT-LLM endpoint 完成了全部四个 BFCL smoke 请求。最终 `0.25` 得分属于模型质量问题,而非请求兼容性问题。上游 issue [9709](https://github.com/NVIDIA/TensorRT-LLM/issues/9709) 使用相同的请求模型扩展机制处理缺失的 OpenAI 兼容字段;目前尚无专门跟踪 `store` 字段的上游 issue。 ### srt-slurm 外部 DP rank 分配 srt-slurm `v1.0.53` 在外部 DP 模式下为每张 GPU 创建一个进程,即使每个 DP rank 应占用 `TP × PP` 张 GPU。Kimi K3 的 TP4 与 DP4 因此会错误地得到十六个单 GPU rank,而不是四个四 GPU rank。补丁会校验 `DP × TP × PP` 与分配资源一致,并为每个 DP rank 创建一个持有完整 GPU 集合的进程。GB200 运行 [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) 在该拓扑上生成了完整 BFCL 产物,得分为 `1.0`。 -NVIDIA/srt-slurm `v1.0.82` 已包含支持数据并行与模型并行组合的 per-node 外部 DP 实现。PR 2634 暂时保留经过硬件验证的 `v1.0.53` 固定版本,待 Kimi recipe 使用新版本完成渲染检查与硬件验证后迁移。 +NVIDIA/srt-slurm [PR 331](https://github.com/NVIDIA/srt-slurm/pull/331) 已随 `v1.0.82` 发布,其中新增了支持数据并行与模型并行组合的 per-node 外部 DP 实现。PR 2634 暂时保留经过硬件验证的 `v1.0.53` 固定版本,待 Kimi recipe 使用新版本完成渲染检查与硬件验证后迁移。 ### srt-slurm provider 评估调度 固定的 srt-slurm 版本只支持硬编码的 `lm-eval` runner,并且仅传递旧版评估环境变量。显式启用的 provider 评估需要 `EVAL_FRAMEWORK`、`EVAL_SUITE`、`EVAL_LIMIT` 以及各自的原生产物。补丁只扩展评估环境变量白名单和评估命令。吞吐量任务不会触发这些逻辑,所有替换都要求精确匹配次数,否则立即失败。 -最新 srt-slurm `v1.0.82` 仍使用 `get_runner("lm-eval")` 构建后置评估 runner,因此仅升级版本无法提供 provider 调度能力。 +上游合并的 lm-eval 实现来自 NVIDIA/srt-slurm [PR 122](https://github.com/NVIDIA/srt-slurm/pull/122)。最新 `v1.0.82` 仍使用 `get_runner("lm-eval")` 构建后置评估 runner,因此仅升级版本无法提供 provider 调度能力。上游仍在开放的 [PR 41](https://github.com/NVIDIA/srt-slurm/pull/41) 是支持外部或自定义评估 harness 的通用化跟踪路径。 ## 安全措施 From 139128e1ca983bcbef50a683baaa5242e0b6bc09 Mon Sep 17 00:00:00 2001 From: Cam Quilici Date: Mon, 31 Aug 2026 11:32:30 -0500 Subject: [PATCH 91/99] fix: cut MI300X routing over to the new AMD cluster --- configs/amd-master.yaml | 4 +- configs/runners.yaml | 42 +++++++++---------- ...ch_mi300x-amds.sh => launch_mi300x-amd.sh} | 9 ++-- .../test_generate_sweep_configs.py | 2 +- 4 files changed, 27 insertions(+), 30 deletions(-) rename runners/{launch_mi300x-amds.sh => launch_mi300x-amd.sh} (87%) diff --git a/configs/amd-master.yaml b/configs/amd-master.yaml index 99f2aecfa4..c81fa54434 100644 --- a/configs/amd-master.yaml +++ b/configs/amd-master.yaml @@ -432,7 +432,7 @@ qwen3.5-fp8-mi300x-sglang-agentic-mtp: image: lmsysorg/sglang-rocm:v0.5.16-rocm720-mi30x-20260730 model: Qwen/Qwen3.5-397B-A17B-FP8 model-prefix: qwen3.5 - runner: cluster:mi300x-amds + runner: cluster:mi300x-amd precision: fp8 framework: sglang multinode: false @@ -1535,7 +1535,7 @@ minimaxm3-fp8-mi300x-vllm-agentic-mtp: image: vllm/vllm-openai-rocm:v0.27.1 model: MiniMaxAI/MiniMax-M3-MXFP8 model-prefix: minimaxm3 - runner: cluster:mi300x-amds + runner: cluster:mi300x-amd precision: fp8 framework: vllm multinode: false diff --git a/configs/runners.yaml b/configs/runners.yaml index 7f4f03f84f..7282885926 100644 --- a/configs/runners.yaml +++ b/configs/runners.yaml @@ -66,15 +66,15 @@ labels: - b200-nscale-slurm_08 - b200-nscale-slurm_09 mi300x: - - mi300x-amds_00 - - mi300x-amds_01 - - mi300x-amds_02 - - mi300x-amds_03 - - mi300x-amds_04 - - mi300x-amds_05 - - mi300x-amds_06 - - mi300x-amds_07 - - mi300x-amds_08 + - mi300x-amd_00 + - mi300x-amd_01 + - mi300x-amd_02 + - mi300x-amd_03 + - mi300x-amd_04 + - mi300x-amd_05 + - mi300x-amd_06 + - mi300x-amd_07 + - mi300x-amd_08 mi325x: - mi325x-amds_00 - mi325x-amds_01 @@ -207,16 +207,16 @@ labels: - gb300-nv_2 cluster:rtx6000pro-lat: - rtx6000pro-lat_00 - cluster:mi300x-amds: - - mi300x-amds_00 - - mi300x-amds_01 - - mi300x-amds_02 - - mi300x-amds_03 - - mi300x-amds_04 - - mi300x-amds_05 - - mi300x-amds_06 - - mi300x-amds_07 - - mi300x-amds_08 + cluster:mi300x-amd: + - mi300x-amd_00 + - mi300x-amd_01 + - mi300x-amd_02 + - mi300x-amd_03 + - mi300x-amd_04 + - mi300x-amd_05 + - mi300x-amd_06 + - mi300x-amd_07 + - mi300x-amd_08 cluster:mi300x-tw: - mi300x-tw_00 - mi300x-tw_01 @@ -269,8 +269,8 @@ hardware: cluster:rtx6000pro-lat: available-cpu-dram-mib: 1_500_000 gpus-per-node: 8 - cluster:mi300x-amds: - available-cpu-dram-mib: 2_321_924 + cluster:mi300x-amd: + available-cpu-dram-mib: 1_500_000 gpus-per-node: 8 cluster:mi300x-tw: available-cpu-dram-mib: 2_322_328 diff --git a/runners/launch_mi300x-amds.sh b/runners/launch_mi300x-amd.sh similarity index 87% rename from runners/launch_mi300x-amds.sh rename to runners/launch_mi300x-amd.sh index 96d92e23d7..12c89d84dd 100644 --- a/runners/launch_mi300x-amds.sh +++ b/runners/launch_mi300x-amd.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -export HF_HUB_CACHE_MOUNT="/raid/hf-hub-cache/" +export HF_HUB_CACHE_MOUNT="/raid/inferencex/hf-hub-cache/" PARTITION="compute" -SQUASH_DIR="/raid/hf-hub-cache/runtime-cache/enroot" +SQUASH_DIR="/raid/inferencex/squash" SQUASH_FILE="$SQUASH_DIR/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g').sqsh" LOCK_FILE="${SQUASH_FILE}.lock" COMPUTE_TMPDIR="$SQUASH_DIR/tmp-${UID}" @@ -21,10 +21,7 @@ export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" set -x -# Exclude known-bad nodes; let Slurm pick from anything else: -# chi-mi300x-049: persistent /nvme_home disk-full -# chi-mi300x-121: provisioning incomplete; missing /raid and Enroot storage -JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --exclude=chi-mi300x-049,chi-mi300x-121 --gres=gpu:$GPU_COUNT --cpus-per-task=256 --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --gres=gpu:$GPU_COUNT --cpus-per-task=128 --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" >&2 diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index 44b7693438..af34ac1eea 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -2193,7 +2193,7 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( assert len(manifest) == 63 assert manifest_digest == ( - '3e4a3195f921e87e97ccfe525b570ba9180ab0acc13206f591d687d9dbe18f14' + '1630cdd6fbf77302ee3286b118710576aa090e1502b3e5ec482b7f537a3f1132' ), manifest_digest for row in rows: if isinstance(row['conc'], list): From 260c5b911c33b4d2e4c22d641ec0abe3e343c132 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:09:36 -0500 Subject: [PATCH 92/99] fix: classify empty tool outputs correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:正确分类空工具调用输出,将成功请求中的零调用结果保留为有效的模型质量失败,而不是集成失败。 --- utils/evals/EVALS.md | 4 ++- utils/evals/minimax_provider_eval.py | 8 +++--- utils/evals/test_minimax_provider_eval.py | 33 ++++++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 9c91777453..f239e6b788 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -227,7 +227,9 @@ artifact. Its `result_format` is `inferencex-eval-v1`, `eval_adapter` is `exact_match,strict-match`. A completed run records original and effective sample counts of one. Its score is the minimum of the stock verifier's tool-call match rate, tool-call schema accuracy, and one minus its -error-only-reasoning rate. The `minimax_m3_smoke` threshold remains `1.0`. +error-only-reasoning rate. A successful request that emits no tool calls has +zero schema accuracy, so it remains an effective model-quality result rather +than an integration failure. The `minimax_m3_smoke` threshold remains `1.0`. Setup, transport, timeout, malformed native output, and collection failures emit a zero-effective-sample compatibility artifact with integration-error diff --git a/utils/evals/minimax_provider_eval.py b/utils/evals/minimax_provider_eval.py index 43c102bd42..077258e4e0 100755 --- a/utils/evals/minimax_provider_eval.py +++ b/utils/evals/minimax_provider_eval.py @@ -254,11 +254,13 @@ def project_native_artifacts(*, output_dir: Path, model: str) -> Path: tool_call_total = _nonnegative_count( report.get("tool_calls_total_count"), "tool_calls_total_count" ) - if tool_call_total == 0 or schema_errors > tool_call_total: + if schema_errors > tool_call_total: raise SmokeSuiteError( - "native summary tool-call schema counts must describe one or more calls" + "native summary tool-call schema counts are inconsistent" ) - schema_rate = 1.0 - (schema_errors / tool_call_total) + schema_rate = ( + 0.0 if tool_call_total == 0 else 1.0 - (schema_errors / tool_call_total) + ) reasoning_error_rate = _rate( report.get("error_only_reasoning_rate"), "error_only_reasoning_rate" ) diff --git a/utils/evals/test_minimax_provider_eval.py b/utils/evals/test_minimax_provider_eval.py index 9f86d9b79b..7e88f49db9 100644 --- a/utils/evals/test_minimax_provider_eval.py +++ b/utils/evals/test_minimax_provider_eval.py @@ -16,12 +16,12 @@ def _native_outputs( match_rate: float = 1.0, schema_rate: float = 1.0, reasoning_error_rate: float = 0.0, + tool_call_total: int = 10, ) -> None: (output_dir / mpe.NATIVE_RESULTS_FILENAME).write_text( json.dumps({"data_index": 1, "status": status}) + "\n", encoding="utf-8", ) - tool_call_total = 10 schema_errors = round((1.0 - schema_rate) * tool_call_total) (output_dir / mpe.NATIVE_REPORT_FILENAME).write_text( json.dumps( @@ -177,6 +177,37 @@ def runner(command: list[str], **_: Any) -> subprocess.CompletedProcess[str]: assert "integration_error" not in compatibility +def test_completed_zero_tool_call_result_is_model_failure( + tmp_path: Path, monkeypatch +) -> None: + output_dir = tmp_path / "output" + source_dir = tmp_path / "source" + dependency_dir = tmp_path / "deps" + source_dir.mkdir() + dependency_dir.mkdir() + monkeypatch.setattr(mpe, "verify_source_tree", lambda _: None) + + def runner(command: list[str], **_: Any) -> subprocess.CompletedProcess[str]: + _native_outputs(output_dir, match_rate=0.0, tool_call_total=0) + return subprocess.CompletedProcess(command, 0) + + passed = mpe.run_evaluation( + python=Path("python"), + source_dir=source_dir, + dependency_dir=dependency_dir, + base_url="http://127.0.0.1:8000/v1", + model="MiniMax-M3", + output_dir=output_dir, + runner=runner, + ) + + assert passed is True + compatibility = _compatibility(output_dir) + assert compatibility["results"][mpe.TASK_NAME]["exact_match,strict-match"] == 0.0 + assert compatibility["n-samples"][mpe.TASK_NAME]["effective"] == 1 + assert "integration_error" not in compatibility + + def test_request_failure_is_reported_as_integration_error( tmp_path: Path, monkeypatch ) -> None: From 2249e46802a532a0c0e07c242a493fc9c5d07ce8 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:01:55 -0500 Subject: [PATCH 93/99] fix: bound full BFCL evaluation runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:限制完整 BFCL 评估运行时间,固定请求超时和重试次数,将 Kimi 多轮步数限制为 10,并为其完整套件预留四小时。 --- benchmarks/benchmark_lib.sh | 2 +- utils/evals/EVALS.md | 15 +++++------ utils/evals/bfcl_adapter.py | 36 ++++++++++++++++++++++++++- utils/evals/test_bfcl_eval.py | 35 ++++++++++++++++++++++++++ utils/evals/test_run_eval_dispatch.py | 8 +++--- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index c60de078b5..e7131952c2 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1709,7 +1709,7 @@ run_bfcl_eval() { _run_bfcl_suite_eval "$eval_suite" 8 7200 true "$@" ;; bfcl_vllm_kimi) - _run_bfcl_suite_eval "$eval_suite" 16 7200 true "$@" + _run_bfcl_suite_eval "$eval_suite" 16 14400 true "$@" ;; *) echo "ERROR: unsupported BFCL suite '${eval_suite}'" >&2 diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index f239e6b788..1788bd87d8 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -328,9 +328,9 @@ The smoke fixes temperature to `0` and uses four BFCL worker threads. Request construction, response interpretation, and retry behavior remain those of the pinned stock BFCL OpenAI-completions handler and OpenAI SDK. The adapter only registers the served model against that stock handler. A 900-second external -process deadline bounds the smoke; the full suites use their declared -two-hour deadline. Dependency installation is separately bounded at 600 -seconds. Dependency, setup, transport, timeout, and collection failures write +process deadline bounds the smoke; each full suite uses its declared deadline. +Dependency installation is separately bounded at 600 seconds. Dependency, +setup, transport, timeout, and collection failures write zero-score artifacts with integration-error metadata and fail the runner nonzero. A completed evaluation exits independently of model quality; the workflow score-validation step applies the threshold afterward. @@ -390,10 +390,11 @@ the V4 agentic web-search and memory evaluations. Select these suites explicitly with `eval-framework: bfcl`; `bfcl_smoke` remains the framework default. Both suites use BFCL's OpenAI completions handler against the local endpoint rather than a hosted-provider handler. They -fix temperature to `0.001`, permit the same two bounded request retries, and -keep the 180-second per-attempt timeout. MiniMax uses eight worker threads. -Kimi uses 16 threads and permits up to ten multi-turn steps. The whole-suite -timeout is 7200 seconds. +fix temperature to `0.001` and retain the stock handler's request construction, +response interpretation, and retry behavior. A transport-only subclass pins +the OpenAI SDK to two retries and a 180-second per-attempt timeout. MiniMax uses +eight worker threads and a two-hour whole-suite timeout. Kimi uses 16 threads, +caps multi-turn cases at ten steps, and uses a four-hour whole-suite timeout. The adapter builds a deterministic run-ID map from the pinned BFCL dataset. Single-turn suites select every case in their named categories. The Kimi diff --git a/utils/evals/bfcl_adapter.py b/utils/evals/bfcl_adapter.py index 35737e6ac0..ea52e7038c 100644 --- a/utils/evals/bfcl_adapter.py +++ b/utils/evals/bfcl_adapter.py @@ -24,6 +24,9 @@ ADAPTER_NAME = "bfcl-v4-openai-completions" DEFAULT_NUM_THREADS = 4 REQUIRED_SCORE = 0.75 +FULL_SUITE_REQUEST_TIMEOUT_SECONDS = 180 +FULL_SUITE_REQUEST_MAX_RETRIES = 2 +KIMI_MAXIMUM_STEP_LIMIT = 10 BFCL_PACKAGE = "bfcl-eval" BFCL_PACKAGE_VERSION = "2026.3.23" @@ -447,6 +450,31 @@ def _clear_upstream_modules() -> None: sys.modules.pop(module_name, None) +def _apply_suite_runtime_limits(suite: SuiteSpec) -> None: + """Apply pinned BFCL limits before importing its model handlers.""" + if suite is KIMI_SUITE: + from bfcl_eval.constants import default_prompts as bfcl_prompts + + bfcl_prompts.MAXIMUM_STEP_LIMIT = KIMI_MAXIMUM_STEP_LIMIT + + +def _bounded_openai_handler(stock_handler: type[Any]) -> type[Any]: + """Retain BFCL's handler while bounding its OpenAI transport.""" + + class BoundedOpenAICompletionsHandler(stock_handler): + def _build_client_kwargs(self) -> dict[str, Any]: + kwargs = super()._build_client_kwargs() + kwargs.update( + timeout=FULL_SUITE_REQUEST_TIMEOUT_SECONDS, + max_retries=FULL_SUITE_REQUEST_MAX_RETRIES, + ) + return kwargs + + return BoundedOpenAICompletionsHandler + + + + def _write_id_map( project_root: Path, case_ids_by_category: Mapping[str, tuple[str, ...]] ) -> None: @@ -582,6 +610,7 @@ def _run_upstream( ) -> None: """Lazily load and invoke the pinned BFCL API against an existing server.""" suite, case_ids_by_category = _read_selected_suite(project_root) + _apply_suite_runtime_limits(suite) os.environ["BFCL_PROJECT_ROOT"] = str(project_root) os.environ["OPENAI_BASE_URL"] = base_url os.environ["OPENAI_API_KEY"] = api_key @@ -592,6 +621,11 @@ def _run_upstream( from bfcl_eval.model_handler.api_inference.openai_completion import ( OpenAICompletionsHandler, ) + handler = ( + OpenAICompletionsHandler + if suite is SMOKE_SUITE + else _bounded_openai_handler(OpenAICompletionsHandler) + ) bfcl_model_config.MODEL_CONFIG_MAPPING[model] = ModelConfig( model_name=model, @@ -599,7 +633,7 @@ def _run_upstream( url="", org="", license="unknown", - model_handler=OpenAICompletionsHandler, + model_handler=handler, input_price=None, output_price=None, is_fc_model=True, diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index f3f4246e9e..98ee931c3f 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -919,6 +919,41 @@ def test_score_total_must_match_every_selected_id(tmp_path: Path) -> None: ) +def test_full_suite_handler_bounds_openai_requests() -> None: + class StockOpenAICompletionsHandler: + def _build_client_kwargs(self) -> dict[str, Any]: + return {"api_key": "stock-key"} + + handler = be._bounded_openai_handler(StockOpenAICompletionsHandler) + + assert issubclass(handler, StockOpenAICompletionsHandler) + assert handler()._build_client_kwargs() == { + "api_key": "stock-key", + "timeout": 180, + "max_retries": 2, + } + + +def test_kimi_suite_caps_multi_turn_steps(monkeypatch: pytest.MonkeyPatch) -> None: + constants = ModuleType("bfcl_eval.constants") + constants.__path__ = [] + prompts = ModuleType("bfcl_eval.constants.default_prompts") + prompts.MAXIMUM_STEP_LIMIT = 20 + constants.default_prompts = prompts + monkeypatch.setitem(sys.modules, "bfcl_eval.constants", constants) + monkeypatch.setitem( + sys.modules, + "bfcl_eval.constants.default_prompts", + prompts, + ) + + be._apply_suite_runtime_limits(be.MINIMAX_SUITE) + assert prompts.MAXIMUM_STEP_LIMIT == 20 + + be._apply_suite_runtime_limits(be.KIMI_SUITE) + assert prompts.MAXIMUM_STEP_LIMIT == 10 + + def test_upstream_registration_uses_exact_stock_openai_handler( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index cc9a7937e8..19fb7ab706 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -3071,11 +3071,11 @@ def test_bfcl_full_suites_use_suite_specific_runtime_and_archive_before_cleanup( tmp_path: Path, ) -> None: suite_contracts = ( - ("bfcl_vllm_minimax_m3", "8"), - ("bfcl_vllm_kimi", "16"), + ("bfcl_vllm_minimax_m3", "8", "7200"), + ("bfcl_vllm_kimi", "16", "14400"), ) - for suite, expected_threads in suite_contracts: + for suite, expected_threads, expected_timeout in suite_contracts: suite_tmp_path = tmp_path / suite suite_tmp_path.mkdir() result, paths = _run_bfcl_adapter_command( @@ -3086,7 +3086,7 @@ def test_bfcl_full_suites_use_suite_specific_runtime_and_archive_before_cleanup( output = result.stdout + result.stderr assert result.returncode == 0, result.stderr - assert "TIMEOUT_ARG=<7200>" in output + assert f"TIMEOUT_ARG=<{expected_timeout}>" in output assert "ADAPTER_ARG=<--suite>" in output assert f"ADAPTER_ARG=<{suite}>" in output assert f"EVAL_COMPLETED_SUITE={suite}" in output From c4cb22c6e83344c914b0cc86d3ad8fa80b89efb3 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:25:40 -0500 Subject: [PATCH 94/99] ci: make tool evaluation scores nonblocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将工具调用评估得分设为非阻塞诊断指标。 --- utils/evals/EVALS.md | 28 ++++++++++++++------------- utils/evals/bfcl_adapter.py | 2 +- utils/evals/test_bfcl_eval.py | 8 ++++---- utils/evals/test_run_eval_dispatch.py | 4 ++-- utils/evals/thresholds.yaml | 6 +++--- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 1788bd87d8..97c0bc212a 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -155,10 +155,11 @@ non-streaming and streaming modes. Each mode runs once through the unchanged upstream pytest harness. The unchanged native report remains one final outcome per mode. It is uploaded as `kimi_vendor_report.json`, and `utils/evals/kimi_vendor_eval.py` projects those two outcomes into the existing -eval result shape. Both must pass, so the `kimi_tool_call_schema` threshold is -`1.0`. Setup, timeout, and collection failures emit a zero-score result with -error metadata. The adapter's 900-second global timeout bounds the entire -upstream pytest process. +eval result shape. Both outcomes are recorded with a `0.0` +`kimi_tool_call_schema` threshold, so model quality remains diagnostic. Setup, +timeout, and collection failures emit a zero-score result with error metadata. +The adapter's 900-second global timeout bounds the entire upstream pytest +process. This smoke validates one object-schema tool call. It does not cover tool choice, parallel calls, multi-turn execution, or general agent quality. Multi-value @@ -229,12 +230,13 @@ sample counts of one. Its score is the minimum of the stock verifier's tool-call match rate, tool-call schema accuracy, and one minus its error-only-reasoning rate. A successful request that emits no tool calls has zero schema accuracy, so it remains an effective model-quality result rather -than an integration failure. The `minimax_m3_smoke` threshold remains `1.0`. +than an integration failure. The `minimax_m3_smoke` threshold is `0.0`, so its +model quality remains diagnostic. Setup, transport, timeout, malformed native output, and collection failures emit a zero-effective-sample compatibility artifact with integration-error -metadata. A complete stock result below the threshold remains a model-quality -outcome rather than an integration failure. +metadata. A complete stock result remains a model-quality outcome rather than +an integration failure. This is a fixed single-case provider compatibility smoke, not the full 102-case MiniMax Provider Verifier, BFCL, or a cross-model quality comparison. @@ -366,12 +368,12 @@ tasks shown above. Every row uses lm-eval-compatible `acc,none` (plus `acc_stderr,none`); BFCL workflows therefore validate with metric prefix `acc,` rather than the default exact-match prefix. -Only `bfcl_smoke` gates the run: its `0.75` threshold requires at least three -of the four fixed upstream cases to be correct. The four `bfcl_` -thresholds are `0.0`, so their one-case scores remain diagnostic and a single -failed category does not become a second gate. BFCL reuses the existing eval -job, upload paths, aggregation, and validation instead of adding a parallel -workflow or artifact route. +The `bfcl_smoke` and four `bfcl_` thresholds are `0.0`, so all five +scores remain diagnostic. Dependency, endpoint, timeout, malformed-output, +missing-sample, and integration failures still fail through the standard +zero-effective-sample path. BFCL reuses the existing eval job, upload paths, +aggregation, and validation instead of adding a parallel workflow or artifact +route. #### BFCL V4 model-quality suites diff --git a/utils/evals/bfcl_adapter.py b/utils/evals/bfcl_adapter.py index ea52e7038c..e27574ce24 100644 --- a/utils/evals/bfcl_adapter.py +++ b/utils/evals/bfcl_adapter.py @@ -23,7 +23,7 @@ RESULT_FORMAT = "inferencex-eval-v1" ADAPTER_NAME = "bfcl-v4-openai-completions" DEFAULT_NUM_THREADS = 4 -REQUIRED_SCORE = 0.75 +REQUIRED_SCORE = 0.0 FULL_SUITE_REQUEST_TIMEOUT_SECONDS = 180 FULL_SUITE_REQUEST_MAX_RETRIES = 2 KIMI_MAXIMUM_STEP_LIMIT = 10 diff --git a/utils/evals/test_bfcl_eval.py b/utils/evals/test_bfcl_eval.py index 98ee931c3f..dac3ffa17d 100644 --- a/utils/evals/test_bfcl_eval.py +++ b/utils/evals/test_bfcl_eval.py @@ -68,7 +68,7 @@ def import_without_yaml(name, *args, **kwargs): monkeypatch.setattr(builtins, "__import__", import_without_yaml) thresholds = vs.load_config(str(Path(vs.__file__).with_name("thresholds.yaml"))) - assert thresholds["default"]["bfcl_smoke"] == 0.75 + assert thresholds["default"]["bfcl_smoke"] == 0.0 assert thresholds["default"]["bfcl_parallel"] == 0.0 @@ -458,7 +458,7 @@ def test_perfect_score_projects_pinned_ids_and_upstream_headers( ] -def test_weighted_score_failure_is_complete_and_left_to_threshold_validator( +def test_weighted_score_is_complete_and_diagnostic( tmp_path: Path, ) -> None: completed, output_dir, _ = _run( @@ -482,8 +482,8 @@ def test_weighted_score_failure_is_complete_and_left_to_threshold_validator( assert "integration_error" not in compatibility native = _native(output_dir) assert native["completed"] is True - assert native["passed"] is False - assert native["threshold"] == 0.75 + assert native["passed"] is True + assert native["threshold"] == 0.0 assert native["summary"]["correct_count"] == 2 diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 19fb7ab706..fc5a852438 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2818,7 +2818,7 @@ def test_bfcl_rejects_unknown_suite() -> None: assert "unsupported BFCL suite 'not_a_bfcl_suite'" in result.stderr -def test_bfcl_full_suite_thresholds_are_diagnostic_and_namespaced() -> None: +def test_bfcl_suite_thresholds_are_diagnostic_and_namespaced() -> None: thresholds = yaml.safe_load( (REPO_ROOT / "utils/evals/thresholds.yaml").read_text() )["default"] @@ -2840,7 +2840,7 @@ def test_bfcl_full_suite_thresholds_are_diagnostic_and_namespaced() -> None: "bfcl_vllm_kimi_multi_turn_long_context", ) - assert thresholds["bfcl_smoke"] == 0.75 + assert thresholds["bfcl_smoke"] == 0.0 assert all(thresholds[task] == 0.0 for task in full_suite_tasks) diff --git a/utils/evals/thresholds.yaml b/utils/evals/thresholds.yaml index 4f770f2b87..d9b41eba40 100644 --- a/utils/evals/thresholds.yaml +++ b/utils/evals/thresholds.yaml @@ -1,11 +1,11 @@ { "default": { "gsm8k": 0.90, - "kimi_tool_call_schema": 1.0, + "kimi_tool_call_schema": 0.0, "kimi_tool_call_schema_full": 0.0, - "minimax_m3_smoke": 1.0, + "minimax_m3_smoke": 0.0, "minimax_m3_full": 0.0, - "bfcl_smoke": 0.75, + "bfcl_smoke": 0.0, "bfcl_simple_python": 0.0, "bfcl_multiple": 0.0, "bfcl_parallel": 0.0, From 3c2a9e9caff8173d3c1ab360570caf9209e0d9d9 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:58:36 -0500 Subject: [PATCH 95/99] feat: auto-run matching model vendor validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:自动为匹配模型运行厂商验证器 --- .github/workflows/e2e-tests.yml | 30 +-- utils/evals/EVALS.md | 62 +++--- utils/evals/test_run_eval_dispatch.py | 57 +++++- utils/matrix_logic/generate_sweep_configs.py | 115 ++++++++--- .../test_generate_sweep_configs.py | 193 +++++++++++++++++- utils/matrix_logic/validation.py | 30 ++- 6 files changed, 398 insertions(+), 89 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index dfd66b1037..5b63172233 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -46,10 +46,10 @@ on: type: string default: "" eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" + description: "Eval runner override (auto uses model-aware selection; lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" required: false type: string - default: "lm-eval" + default: "auto" eval-suite: description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" required: false @@ -136,10 +136,10 @@ on: type: string default: "" eval-framework: - description: "Eval runner (lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" + description: "Eval runner override (auto uses model-aware selection; lm-eval, swebench, kimi-vendor, minimax-vendor, or bfcl)" required: false type: string - default: "lm-eval" + default: "auto" eval-suite: description: "Eval suite (kimi_tool_call_schema, kimi_tool_call_schema_full, minimax_m3_smoke, minimax_m3_full, bfcl_smoke, bfcl_vllm_minimax_m3, or bfcl_vllm_kimi); empty for lm-eval and swebench" required: false @@ -285,9 +285,9 @@ jobs: --queue-namespace "${{ github.run_id }}:${{ github.run_attempt }}:${family}" \ --labels-json "$PR_LABELS" } - AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' not in x and not x.get('run-eval', False)]))" | score_matrix agentic) + AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' not in x and not x.get('eval-only', False)]))" | score_matrix agentic) AGENTIC_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' not in x and x.get('run-eval', False)]))" | score_matrix agentic-eval) - MULTI_AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and not x.get('run-eval', False)]))" | score_matrix multi-agentic) + MULTI_AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and not x.get('eval-only', False)]))" | score_matrix multi-agentic) MULTI_AGENTIC_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and x.get('run-eval', False)]))" | score_matrix multi-agentic-eval) SINGLE=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and not x.get('eval-only', False)]))" | score_matrix single) EVALS=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix eval) @@ -412,10 +412,10 @@ jobs: decode-additional-settings: ${{ toJson(matrix.config.decode.additional-settings) }} run-eval: true eval-only: true - eval-conc: ${{ inputs.eval-framework == 'lm-eval' && matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} + eval-conc: ${{ (inputs.eval-framework == 'auto' && (matrix.config['eval-framework'] || 'lm-eval') || inputs.eval-framework) == 'lm-eval' && matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || matrix.config['eval-conc'] }} eval-limit: ${{ inputs.eval-limit }} - eval-framework: ${{ inputs.eval-framework }} - eval-suite: ${{ inputs.eval-suite }} + eval-framework: ${{ inputs.eval-framework == 'auto' && (matrix.config['eval-framework'] || 'lm-eval') || inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite != '' && inputs.eval-suite || (inputs.eval-framework == 'auto' && matrix.config['eval-suite'] || '') }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} ref: ${{ inputs.ref }} @@ -505,8 +505,8 @@ jobs: eval-only: true eval-limit: ${{ inputs.eval-limit }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} - eval-framework: ${{ inputs.eval-framework }} - eval-suite: ${{ inputs.eval-suite }} + eval-framework: ${{ inputs.eval-framework == 'auto' && (matrix.config['eval-framework'] || 'lm-eval') || inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite != '' && inputs.eval-suite || (inputs.eval-framework == 'auto' && matrix.config['eval-suite'] || '') }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -628,8 +628,8 @@ jobs: eval-conc: ${{ matrix.config['eval-conc'] }} eval-limit: ${{ inputs.eval-limit }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} - eval-framework: ${{ inputs.eval-framework }} - eval-suite: ${{ inputs.eval-suite }} + eval-framework: ${{ inputs.eval-framework == 'auto' && (matrix.config['eval-framework'] || 'lm-eval') || inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite != '' && inputs.eval-suite || (inputs.eval-framework == 'auto' && matrix.config['eval-suite'] || '') }} scenario-type: agentic-coding ref: ${{ inputs.ref }} @@ -710,8 +710,8 @@ jobs: run-eval: true eval-only: true eval-limit: ${{ inputs.eval-limit }} - eval-framework: ${{ inputs.eval-framework }} - eval-suite: ${{ inputs.eval-suite }} + eval-framework: ${{ inputs.eval-framework == 'auto' && (matrix.config['eval-framework'] || 'lm-eval') || inputs.eval-framework }} + eval-suite: ${{ inputs.eval-suite != '' && inputs.eval-suite || (inputs.eval-framework == 'auto' && matrix.config['eval-suite'] || '') }} swebench-gen-mode: ${{ inputs.swebench-gen-mode }} ref: ${{ inputs.ref }} diff --git a/utils/evals/EVALS.md b/utils/evals/EVALS.md index 97c0bc212a..1bd2a69344 100644 --- a/utils/evals/EVALS.md +++ b/utils/evals/EVALS.md @@ -7,19 +7,25 @@ from throughput. Selection lives in `mark_eval_entries()` in ## Selection -- **Single-node:** 8k1k only, at the highest and median concurrency for every model, - runner, framework, precision, TP, and decoding configuration. -- **Multi-node:** 8k1k only, with one job per parallelism topology at its highest - eligible concurrency. Rows differing only by concurrency share a topology. -- **Agentic (GSM8K), single-node:** highest-conc entry per (model, runner, - framework, precision) group. -- **Agentic (GSM8K), multi-node:** highest eligible concurrency per - parallelism topology. +- **Fixed-sequence, single-node:** 8k1k only, at the highest and median + concurrency for every model, runner, framework, precision, TP, and decoding + configuration. +- **Fixed-sequence, multi-node:** 8k1k only, with one job per parallelism + topology at its highest eligible concurrency. Rows differing only by + concurrency share a topology. +- **Kimi K3 agentic:** every generated point automatically runs + `kimi-vendor` with `kimi_tool_call_schema`. +- **MiniMax M3 agentic:** every generated point automatically runs + `minimax-vendor` with `minimax_m3_smoke`. +- **Other agentic models (GSM8K):** opt-in through `--evals-only` or + `--all-evals`, at the highest concurrency per deployment group. +- **BFCL:** explicit only. No automatic model mapping selects BFCL. Generator eval modes: -- Default: throughput plus the selected eval subset. -- `--no-evals`: throughput only. +- Default: throughput plus the fixed-sequence subset and every automatically + selected Kimi K3 or MiniMax M3 vendor eval. +- `--no-evals`: throughput only, including no automatic vendor evals. - `--evals-only`: selected evals only. - `--all-evals`: every eligible fixed-sequence and agentic eval. This is equivalent to `--evals-only --all-evals`. Multi-node fixed-sequence @@ -96,15 +102,19 @@ malformed metadata, duplicates, and raw/aggregate mismatches are not. See [workflow reuse](../../.github/workflows/README.md#reusing-an-approved-pr-full-sweep). ## How? -`run_eval` in `benchmarks/benchmark_lib.sh` dispatches to the selected eval -runner. Existing jobs continue to use lm-eval with GSM8K by default. - -The default eval framework is [lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) (`lm-eval`). Agentic eval-only matrix jobs inherit this default and therefore run the same GSM8K task as 8k1k. Explicit agentic runs can still select SWE-bench. -The Kimi smoke is opt-in and applies to supported models exposing a -tool-aware OpenAI-compatible chat-completions API. Select -`eval-framework: kimi-vendor` and `eval-suite: kimi_tool_call_schema` on -`e2e-tests.yml`, or invoke it from the repository root after a server is ready: +`run_eval` in `benchmarks/benchmark_lib.sh` dispatches to the selected eval +runner. `e2e-tests.yml` defaults `eval-framework` to `auto`, then reads the +concrete framework and suite from each eval matrix row. Fixed-sequence and +opted-in generic agentic evals use +[lm-evaluation-harness](https://github.com/EleutherAI/lm-evaluation-harness) +(`lm-eval`) with GSM8K. Workflow inputs can explicitly override the +matrix-selected framework or suite for manual diagnostics. + +The Kimi smoke runs automatically for every generated `kimik3` agentic point. +The matrix selects `eval-framework: kimi-vendor` and +`eval-suite: kimi_tool_call_schema`. To invoke the same smoke manually from the +repository root after a server is ready: ```bash source benchmarks/benchmark_lib.sh @@ -120,7 +130,8 @@ The framework selects a suite-specific subprocess adapter, while the suite selects a case set understood by that adapter. Each adapter owns its endpoint format, dependencies, native report, metrics, and integration-failure policy. Kimi, MiniMax, and BFCL use separate explicit `run_eval` cases rather than a -shared request or report abstraction. +shared request or report abstraction. Automatic selection chooses only the Kimi +and MiniMax vendor cases; BFCL remains an explicit workflow override. Agentic eval jobs forward the matrix `spec-decoding` value, so MTP entries launch their existing `*_mtp.sh` server instead of silently falling back to STP. @@ -186,11 +197,10 @@ envelope, artifact staging, collector, and dashboard path. ### MiniMax provider compatibility smoke -The Phase 1 MiniMax smoke is opt-in and applies to supported models exposing -an OpenAI-compatible chat-completions API. Select -`eval-framework: minimax-vendor` and `eval-suite: minimax_m3_smoke` in -`e2e-tests.yml`, or run it from the repository root against an already-ready -server: +The Phase 1 MiniMax smoke runs automatically for every generated `minimaxm3` +agentic point. The matrix selects `eval-framework: minimax-vendor` and +`eval-suite: minimax_m3_smoke`. To invoke the same smoke manually from the +repository root against an already-ready server: ```bash source benchmarks/benchmark_lib.sh @@ -566,8 +576,8 @@ attempt cannot replace a newer failed retry. |----------|---------|-------------| | `RUN_EVAL` | `false` | Enable eval after throughput benchmark | | `EVAL_ONLY` | `false` | Skip throughput, only run evals (set by workflow) | -| `EVAL_FRAMEWORK` | `lm-eval` | Eval runner (`lm-eval`, `swebench`, `kimi-vendor`, or `minimax-vendor`) | -| `EVAL_SUITE` | basename of `EVAL_TASKS_DIR`, else `gsm8k` | Provider suite selector and artifact identity. External overrides are supported by `kimi-vendor` and `minimax-vendor`; other runners derive it from their task | +| `EVAL_FRAMEWORK` | Workflow: `auto`; benchmark runner: `lm-eval` | Eval runner (`lm-eval`, `swebench`, `kimi-vendor`, `minimax-vendor`, or `bfcl`). `auto` resolves from matrix metadata before reusable workflow dispatch | +| `EVAL_SUITE` | Matrix-selected for automatic vendor evals; otherwise basename of `EVAL_TASKS_DIR` or `gsm8k` | Provider suite selector and artifact identity. Explicit workflow overrides remain supported | | `EVAL_TASKS_DIR` | `utils/evals/gsm8k.yaml` | Path to lm-eval task YAML | | `EVAL_RESULT_DIR` | `/tmp/eval_out-*` | Output directory for eval results | | `EVAL_MAX_MODEL_LEN` | `16384` | Max context for eval (set by `compute_eval_context_length`) | diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index fc5a852438..1d0d7a033a 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -21,6 +21,16 @@ SINGLE_NODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-tmpl.yml" MULTINODE_WORKFLOW = REPO_ROOT / ".github/workflows/benchmark-multinode-tmpl.yml" E2E_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "e2e-tests.yml" +AUTO_EVAL_FRAMEWORK_EXPR = ( + "${{ inputs.eval-framework == 'auto' && " + "(matrix.config['eval-framework'] || 'lm-eval') || " + "inputs.eval-framework }}" +) +AUTO_EVAL_SUITE_EXPR = ( + "${{ inputs.eval-suite != '' && inputs.eval-suite || " + "(inputs.eval-framework == 'auto' && matrix.config['eval-suite'] || '') }}" +) + _SCRIPT = r""" source "$BENCHMARK_LIB" @@ -2677,8 +2687,8 @@ def test_agentic_eval_workflow_forwards_runner_contract() -> None: forwarded = workflow["jobs"]["test-sweep-agentic-evals"]["with"] assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" - assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" - assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + assert forwarded["eval-framework"] == AUTO_EVAL_FRAMEWORK_EXPR + assert forwarded["eval-suite"] == AUTO_EVAL_SUITE_EXPR assert forwarded["kv-offload-backend"] == ( "${{ matrix.config['kv-offload-backend'].name }}" ) @@ -2692,8 +2702,8 @@ def test_fixed_eval_workflows_forward_provider_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) for job_name in ("test-sweep-evals", "test-sweep-multi-node-evals"): forwarded = workflow["jobs"][job_name]["with"] - assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" - assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + assert forwarded["eval-framework"] == AUTO_EVAL_FRAMEWORK_EXPR + assert forwarded["eval-suite"] == AUTO_EVAL_SUITE_EXPR reusable_workflow = yaml.safe_load(SINGLE_NODE_WORKFLOW.read_text()) assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" @@ -2710,8 +2720,8 @@ def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: forwarded = workflow["jobs"]["test-sweep-multi-node-agentic-evals"]["with"] reusable_workflow = yaml.safe_load(MULTINODE_WORKFLOW.read_text()) - assert forwarded["eval-framework"] == "${{ inputs.eval-framework }}" - assert forwarded["eval-suite"] == "${{ inputs.eval-suite }}" + assert forwarded["eval-framework"] == AUTO_EVAL_FRAMEWORK_EXPR + assert forwarded["eval-suite"] == AUTO_EVAL_SUITE_EXPR assert reusable_workflow["env"]["EVAL_FRAMEWORK"] == "${{ inputs.eval-framework }}" assert reusable_workflow["env"]["EVAL_SUITE"] == "${{ inputs.eval-suite }}" assert "*_report.json" in MULTINODE_WORKFLOW.read_text() @@ -2722,6 +2732,41 @@ def test_multinode_agentic_eval_workflow_forwards_runner_contract() -> None: assert "bfcl_vllm_kimi" in MULTINODE_WORKFLOW.read_text() +def test_e2e_eval_workflow_defaults_to_model_aware_selection() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + triggers = workflow[True] + + for trigger_name in ("workflow_dispatch", "workflow_call"): + eval_input = triggers[trigger_name]["inputs"]["eval-framework"] + assert eval_input["default"] == "auto" + + forwarded = workflow["jobs"]["test-sweep-multi-node-evals"]["with"] + assert forwarded["eval-conc"] == ( + "${{ (inputs.eval-framework == 'auto' && " + "(matrix.config['eval-framework'] || 'lm-eval') || " + "inputs.eval-framework) == 'lm-eval' && " + "matrix.config['eval-all-concs'] && join(matrix.config.conc, ' ') || " + "matrix.config['eval-conc'] }}" + ) + + +def test_agentic_throughput_split_keeps_eval_marked_rows() -> None: + workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) + get_jobs = next( + step + for step in workflow["jobs"]["get-jobs"]["steps"] + if step.get("id") == "get-jobs" + ) + split_lines = get_jobs["run"].splitlines() + + for variable in ("AGENTIC", "MULTI_AGENTIC"): + line = next(line for line in split_lines if line.strip().startswith( + f"{variable}=$(" + )) + assert "not x.get('eval-only', False)" in line + assert "not x.get('run-eval', False)" not in line + + def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) get_jobs = next( diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 622d06527f..ecd5bd7bad 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -27,6 +27,11 @@ } MIN_EVAL_CONC = 16 +DEFAULT_EVAL_FRAMEWORK = "lm-eval" +AUTOMATIC_AGENTIC_VENDOR_EVALS = { + "kimik3": ("kimi-vendor", "kimi_tool_call_schema"), + "minimaxm3": ("minimax-vendor", "minimax_m3_smoke"), +} # Bound how many multinode agentic conc points share one server allocation. # 1 = one task/SLURM allocation per concurrency (matches single-node agentic). MAX_MULTINODE_AGENTIC_CONCURRENCIES_PER_ALLOCATION = 1 @@ -73,6 +78,8 @@ def trim_conc(entries: list[dict]) -> list[dict]: "eval-only", "eval-conc", "eval-all-concs", + Fields.EVAL_FRAMEWORK.value, + Fields.EVAL_SUITE.value, } groups: dict[tuple, list[int]] = {} out: list[dict] = [] @@ -509,6 +516,8 @@ def _multinode_parallelism_key(entry: dict) -> tuple: Fields.EVAL_CONC.value, Fields.EVAL_ALL_CONCS.value, Fields.EXP_NAME.value, + Fields.EVAL_FRAMEWORK.value, + Fields.EVAL_SUITE.value, } return tuple(sorted( (key, _freeze_matrix_value(value)) @@ -517,25 +526,25 @@ def _multinode_parallelism_key(entry: dict) -> tuple: )) +def automatic_agentic_vendor_eval(entry: dict) -> tuple[str, str] | None: + """Return the default vendor evaluator for supported agentic models.""" + if entry.get(Fields.SCENARIO_TYPE.value) != "agentic-coding": + return None + return AUTOMATIC_AGENTIC_VENDOR_EVALS.get( + entry.get(Fields.MODEL_PREFIX.value) + ) + + def mark_eval_entries(matrix_values: list[dict], include_agentic: bool = False) -> list[dict]: - """Eval selection policy: - - Single-node: only consider 8k1k (isl=8192, osl=1024). - For each unique (model, runner, framework, precision, isl, osl, spec-decoding, dp-attn): - - Ignore entries with conc < MIN_EVAL_CONC - - Mark all entries at the highest CONC (all TPs) - - Mark all entries at the median CONC (all TPs) - - Multi-node: only consider 8k1k entries. For every distinct parallelism - configuration: - - Ignore entries with all conc values < MIN_EVAL_CONC - - Mark the entry containing its highest eligible concurrency - - Set eval-conc to that highest eligible concurrency - - Agentic evals are opt-in to preserve default throughput coverage. - - Single-node: run GSM8K through the same lm-eval path as fixed-sequence - 8k1k evals, marking the highest-conc entry per (model, runner, - framework, precision) group. - - Multi-node: run GSM8K through the same lm-eval path, selecting the - highest eligible concurrency per distinct parallelism config via - eval-conc. + """Apply the default eval selection policy. + + Kimi K3 and MiniMax M3 agentic rows use their vendor validators at every + generated concurrency. Other agentic rows remain opt-in and use GSM8K at + the highest concurrency in each deployment group. + + Fixed-sequence selection is unchanged: single-node 8k1k rows use the + highest and median concurrency per model/runtime group, while multi-node + 8k1k rows use the highest eligible concurrency per parallelism topology. """ from collections import defaultdict @@ -548,6 +557,18 @@ def _eligible_eval_concs(entry): conc_values = conc if isinstance(conc, list) else [conc] return sorted(c for c in conc_values if c >= MIN_EVAL_CONC) + automatic_eval_specs: dict[int, tuple[str, str]] = {} + for i, entry in enumerate(matrix_values): + eval_spec = automatic_agentic_vendor_eval(entry) + if eval_spec is None: + continue + automatic_eval_specs[i] = eval_spec + eval_indices.add(i) + if Fields.PREFILL.value in entry: + conc = entry[Fields.CONC.value] + conc_values = conc if isinstance(conc, list) else [conc] + mn_eval_conc[i] = max(conc_values) + # Single-node: group by (model, runner, framework, precision, isl, osl, spec-decoding, dp-attn). # Only 8k1k entries with a top-level TP (single-node schema). sn_groups = defaultdict(list) @@ -606,6 +627,8 @@ def _eligible_eval_concs(entry): # The selected eval subset uses exactly one conc per group. ag_mn_groups = defaultdict(list) for i, entry in enumerate(matrix_values): + if i in automatic_eval_specs: + continue if entry.get(Fields.SCENARIO_TYPE.value) != 'agentic-coding': continue if Fields.PREFILL.value in entry: @@ -631,7 +654,14 @@ def _eligible_eval_concs(entry): mn_eval_conc[best_idx] = best_eval_conc for i, entry in enumerate(matrix_values): - entry[Fields.RUN_EVAL.value] = i in eval_indices + run_eval = i in eval_indices + entry[Fields.RUN_EVAL.value] = run_eval + if run_eval: + eval_framework, eval_suite = automatic_eval_specs.get( + i, (DEFAULT_EVAL_FRAMEWORK, "") + ) + entry[Fields.EVAL_FRAMEWORK.value] = eval_framework + entry[Fields.EVAL_SUITE.value] = eval_suite if i in mn_eval_conc: entry[Fields.EVAL_CONC.value] = mn_eval_conc[i] @@ -639,18 +669,16 @@ def _eligible_eval_concs(entry): def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: - """Expand eval selection to every 8k1k fixed-sequence entry. - - Evals only run at 8k1k (matching mark_eval_entries), so entries at other - sequence lengths (e.g. 1k1k) are passed through untouched rather than - expanded into eval rows. - Single- and multi-node agentic entries use GSM8K through lm-eval. - Multi-node agentic rows with the same topology are merged (to recombine - any chunking split), but only the highest resulting conc is marked for - eval via eval-conc, matching the default agentic selection policy. - Multi-node fixed-seq-len rows with the same engine topology are merged - into one eval row whose full concurrency list is run sequentially - against the same engine. + """Expand eval selection across all eligible entries. + + Kimi K3 and MiniMax M3 agentic rows remain one eval job per generated + concurrency, using the model's vendor validator. Other agentic entries use + GSM8K through lm-eval. Their multi-node rows are merged by topology and + select the highest resulting concurrency. + + Fixed-sequence evals only run at 8k1k. Multi-node rows with the same engine + topology are merged into one eval row that runs every concurrency + sequentially against the live engine. """ expanded_entries: list[dict] = [] multinode_indices: dict[tuple, int] = {} @@ -659,6 +687,23 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: target_isl, target_osl = seq_len_stoi["8k1k"] for entry in matrix_values: + automatic_eval = automatic_agentic_vendor_eval(entry) + if automatic_eval is not None: + eval_framework, eval_suite = automatic_eval + eval_entry = { + **entry, + Fields.RUN_EVAL.value: True, + Fields.EVAL_FRAMEWORK.value: eval_framework, + Fields.EVAL_SUITE.value: eval_suite, + } + if Fields.PREFILL.value in entry: + conc = entry[Fields.CONC.value] + conc_values = conc if isinstance(conc, list) else [conc] + eval_entry[Fields.CONC.value] = sorted(set(conc_values)) + eval_entry[Fields.EVAL_CONC.value] = max(conc_values) + expanded_entries.append(eval_entry) + continue + if entry.get(Fields.SCENARIO_TYPE.value) == 'agentic-coding': if Fields.PREFILL.value not in entry: entry[Fields.RUN_EVAL.value] = True @@ -719,6 +764,14 @@ def mark_all_eval_entries(matrix_values: list[dict]) -> list[dict]: entry[Fields.RUN_EVAL.value] = True expanded_entries.append(entry) + for entry in expanded_entries: + if not entry.get(Fields.RUN_EVAL.value): + continue + if not entry.get(Fields.EVAL_FRAMEWORK.value): + entry[Fields.EVAL_FRAMEWORK.value] = DEFAULT_EVAL_FRAMEWORK + if entry.get(Fields.EVAL_SUITE.value) is None: + entry[Fields.EVAL_SUITE.value] = "" + return expanded_entries diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index 23f75e9913..520e4105e0 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -472,6 +472,100 @@ def test_default_mode_does_not_mark_agentic(self): f"Expected 0 agentic entries marked run-eval in default mode, got {len(marked)}" ) + def test_default_marks_every_supported_vendor_point(self): + matrix_values = [ + { + "scenario-type": "agentic-coding", + "model-prefix": model_prefix, + "model": model_prefix, + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": conc, + } + for model_prefix in ("kimik3", "minimaxm3") + for conc in (1, 64) + ] + matrix_values.append({ + "scenario-type": "agentic-coding", + "model-prefix": "minimaxm3-bfcl", + "model": "unsupported", + "runner": "b300", + "framework": "vllm", + "precision": "fp4", + "tp": 8, + "conc": 64, + }) + + result = mark_eval_entries(matrix_values) + + expected = { + "kimik3": ("kimi-vendor", "kimi_tool_call_schema"), + "minimaxm3": ("minimax-vendor", "minimax_m3_smoke"), + } + for model_prefix, eval_spec in expected.items(): + rows = [row for row in result if row["model-prefix"] == model_prefix] + assert {row["conc"] for row in rows} == {1, 64} + assert all(row["run-eval"] is True for row in rows) + assert { + (row["eval-framework"], row["eval-suite"]) for row in rows + } == {eval_spec} + + unsupported = result[-1] + assert unsupported["run-eval"] is False + assert "eval-framework" not in unsupported + assert all(row.get("eval-framework") != "bfcl" for row in result) + + def test_default_marks_every_multinode_vendor_point(self): + common = { + "scenario-type": "agentic-coding", + "model-prefix": "kimik3", + "model": "kimi", + "runner": "gb200", + "framework": "sglang-disagg", + "precision": "fp4", + "spec-decoding": "none", + "disagg": True, + "prefill": {"num-worker": 1, "tp": 8}, + "decode": {"num-worker": 1, "tp": 8}, + } + matrix_values = [ + {**common, "conc": [2], "exp-name": "kimi-conc2"}, + {**common, "conc": [32], "exp-name": "kimi-conc32"}, + ] + + result = mark_eval_entries(matrix_values) + + assert len(result) == 2 + assert all(row["run-eval"] is True for row in result) + assert [row["eval-conc"] for row in result] == [2, 32] + assert all(row["eval-framework"] == "kimi-vendor" for row in result) + assert all( + row["eval-suite"] == "kimi_tool_call_schema" for row in result + ) + + def test_fixed_sequence_eval_uses_lm_eval_metadata(self): + matrix_values = [{ + "model": "m", + "runner": "b200", + "framework": "vllm", + "precision": "fp8", + "isl": 8192, + "osl": 1024, + "spec-decoding": "none", + "dp-attn": False, + "tp": 8, + "conc": MIN_EVAL_CONC, + }] + + result = mark_eval_entries(matrix_values) + + assert result[0]["run-eval"] is True + assert result[0]["eval-framework"] == "lm-eval" + assert result[0]["eval-suite"] == "" + + def test_single_node_skips_eval_entries_below_min_conc(self): """Single-node eval selection should ignore conc values below MIN_EVAL_CONC.""" matrix_values = [ @@ -896,11 +990,11 @@ def test_marks_agentic_entries_for_gsm8k(self): assert result[0]['run-eval'] is True assert 'eval-conc' not in result[0] - def test_marks_multinode_agentic_entries_for_swebench(self): - """Unlike fixed-seq-len multi-node (which batches every concurrency - into one lm-eval row via eval-all-concs), multi-node agentic rows for - the same topology are merged but only their highest conc is marked - via eval-conc, since SWE-bench doesn't support batched concurrencies.""" + def test_marks_multinode_agentic_entries_for_gsm8k(self): + """Unlike fixed-seq-len multi-node evals, generic agentic rows with the + same topology merge but select only their highest concurrency through + eval-conc. + """ common = { 'scenario-type': 'agentic-coding', 'model': 'm', 'runner': 'r', 'framework': 'sglang-disagg', @@ -922,6 +1016,33 @@ def test_marks_multinode_agentic_entries_for_swebench(self): assert result[0]['eval-conc'] == 32 assert 'eval-all-concs' not in result[0] + def test_keeps_every_multinode_vendor_point_separate(self): + common = { + "scenario-type": "agentic-coding", + "model-prefix": "minimaxm3", + "model": "minimax", + "runner": "gb200", + "framework": "sglang-disagg", + "precision": "fp4", + "spec-decoding": "none", + "disagg": True, + "prefill": {"num-worker": 1, "tp": 8}, + "decode": {"num-worker": 1, "tp": 8}, + } + entries = [ + {**common, "conc": [2], "exp-name": "minimax-conc2"}, + {**common, "conc": [32], "exp-name": "minimax-conc32"}, + ] + + result = mark_all_eval_entries(mark_eval_entries(entries)) + + assert len(result) == 2 + assert [row["conc"] for row in result] == [[2], [32]] + assert [row["eval-conc"] for row in result] == [2, 32] + assert all(row["eval-framework"] == "minimax-vendor" for row in result) + assert all(row["eval-suite"] == "minimax_m3_smoke" for row in result) + + # ============================================================================= # Test generate_full_sweep for single-node @@ -2130,6 +2251,53 @@ def test_trim_conc_updates_multinode_dispatch_concurrency(self): assert result[0]['eval-conc'] == 4 assert result[0]['run-eval'] is True + def test_kimi_minimax_default_matrix_marks_every_current_point( + self, + monkeypatch, + ): + import sys + + import generate_sweep_configs + + repo_root = Path(__file__).resolve().parents[2] + monkeypatch.setattr(sys, 'argv', [ + 'generate_sweep_configs.py', + 'full-sweep', + '--config-files', + str(repo_root / 'configs/nvidia-master.yaml'), + str(repo_root / 'configs/amd-master.yaml'), + '--runner-config', + str(repo_root / 'configs/runners.yaml'), + '--model-prefix', + 'kimik3', + 'minimaxm3', + '--scenario-type', + 'agentic-coding', + ]) + + rows = generate_sweep_configs.main() + + expected_eval_specs = { + 'kimik3': ('kimi-vendor', 'kimi_tool_call_schema'), + 'minimaxm3': ('minimax-vendor', 'minimax_m3_smoke'), + } + assert {row['model-prefix'] for row in rows} == set(expected_eval_specs) + assert all(row['run-eval'] is True for row in rows) + assert all(row.get('eval-only') is not True for row in rows) + assert { + ( + row['model-prefix'], + row['eval-framework'], + row['eval-suite'], + ) + for row in rows + } == { + (model_prefix, *eval_spec) + for model_prefix, eval_spec in expected_eval_specs.items() + } + assert all(row['eval-framework'] != 'bfcl' for row in rows) + + def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( self, monkeypatch, @@ -2200,6 +2368,21 @@ def test_kimi_minimax_trimmed_eval_matrix_covers_current_configs( assert row['conc'] == [row['eval-conc']] assert all(row['run-eval'] is True for row in rows) assert all(row['eval-only'] is True for row in rows) + expected_eval_specs = { + 'kimik3': ('kimi-vendor', 'kimi_tool_call_schema'), + 'minimaxm3': ('minimax-vendor', 'minimax_m3_smoke'), + } + assert { + ( + row['model-prefix'], + row['eval-framework'], + row['eval-suite'], + ) + for row in rows + } == { + (model_prefix, *eval_spec) + for model_prefix, eval_spec in expected_eval_specs.items() + } def test_all_evals_batches_each_multinode_concurrency( self, diff --git a/utils/matrix_logic/validation.py b/utils/matrix_logic/validation.py index 436ea97b39..7014e54298 100644 --- a/utils/matrix_logic/validation.py +++ b/utils/matrix_logic/validation.py @@ -90,6 +90,8 @@ class Fields(Enum): EVAL_ONLY = 'eval-only' EVAL_CONC = 'eval-conc' EVAL_ALL_CONCS = 'eval-all-concs' + EVAL_FRAMEWORK = 'eval-framework' + EVAL_SUITE = 'eval-suite' """ @@ -178,6 +180,10 @@ class SingleNodeMatrixEntry(BaseModel): disagg: Literal[False] run_eval: bool = Field(alias=Fields.RUN_EVAL.value) eval_only: bool = Field(alias=Fields.EVAL_ONLY.value, default=False) + eval_framework: Optional[str] = Field( + default=None, alias=Fields.EVAL_FRAMEWORK.value + ) + eval_suite: Optional[str] = Field(default=None, alias=Fields.EVAL_SUITE.value) router: Optional[ComponentMetadata] = None recipe_fingerprint: Optional[str] = Field( default=None, @@ -274,6 +280,10 @@ class MultiNodeMatrixEntry(BaseModel): eval_all_concs: bool = Field( default=False, alias=Fields.EVAL_ALL_CONCS.value ) + eval_framework: Optional[str] = Field( + default=None, alias=Fields.EVAL_FRAMEWORK.value + ) + eval_suite: Optional[str] = Field(default=None, alias=Fields.EVAL_SUITE.value) router: Optional[ComponentMetadata] = None kv_p2p_transfer: Optional[str] = Field( default=None, alias=Fields.KV_P2P_TRANSFER.value, min_length=1 @@ -329,10 +339,14 @@ class SingleNodeAgenticMatrixEntry(BaseModel): duration: int = Field(alias=Fields.DURATION.value) exp_name: str = Field(alias=Fields.EXP_NAME.value) scenario_type: str = Field(alias=Fields.SCENARIO_TYPE.value) - # Agentic GSM8K eval rows carry run-eval/eval-only; benchmark rows - # omit them, and exclude_none keeps them out of dumped benchmark output. + # Agentic eval rows carry selection and evaluator metadata. Benchmark-only + # rows omit them, and exclude_none keeps them out of dumped matrix output. run_eval: Optional[bool] = Field(default=None, alias=Fields.RUN_EVAL.value) eval_only: Optional[bool] = Field(default=None, alias=Fields.EVAL_ONLY.value) + eval_framework: Optional[str] = Field( + default=None, alias=Fields.EVAL_FRAMEWORK.value + ) + eval_suite: Optional[str] = Field(default=None, alias=Fields.EVAL_SUITE.value) recipe_fingerprint: Optional[str] = Field( default=None, alias=Fields.RECIPE_FINGERPRINT.value, @@ -378,13 +392,17 @@ class MultiNodeAgenticMatrixEntry(BaseModel): exp_name: str = Field(alias=Fields.EXP_NAME.value) disagg: bool scenario_type: str = Field(alias=Fields.SCENARIO_TYPE.value) - # Agentic eval rows (SWE-bench) carry run-eval/eval-only/eval-conc; - # benchmark rows omit them, and exclude_none keeps them out of dumped - # throughput output. SWE-bench doesn't support batched concurrencies - # (unlike lm-eval), so there is no eval-all-concs field here. + # Agentic eval rows carry selection, concurrency, and evaluator metadata. + # Benchmark-only rows omit them, and exclude_none keeps them out of dumped + # matrix output. Multi-node agentic evals run one selected concurrency per + # job, so they do not use eval-all-concs. run_eval: Optional[bool] = Field(default=None, alias=Fields.RUN_EVAL.value) eval_only: Optional[bool] = Field(default=None, alias=Fields.EVAL_ONLY.value) eval_conc: Optional[int] = Field(default=None, alias=Fields.EVAL_CONC.value) + eval_framework: Optional[str] = Field( + default=None, alias=Fields.EVAL_FRAMEWORK.value + ) + eval_suite: Optional[str] = Field(default=None, alias=Fields.EVAL_SUITE.value) recipe_fingerprint: Optional[str] = Field( default=None, alias=Fields.RECIPE_FINGERPRINT.value, From f967d65cb5be34864f196d89ffbf0010a4e3d7ca Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:00:40 -0500 Subject: [PATCH 96/99] fix: avoid duplicate eval matrix scoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:避免重复计算评估任务矩阵优先级 --- .github/workflows/e2e-tests.yml | 1 - utils/evals/test_run_eval_dispatch.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5b63172233..0b5b1e3113 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -290,7 +290,6 @@ jobs: MULTI_AGENTIC=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and not x.get('eval-only', False)]))" | score_matrix multi-agentic) MULTI_AGENTIC_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if x.get('scenario-type') == 'agentic-coding' and 'prefill' in x and x.get('run-eval', False)]))" | score_matrix multi-agentic-eval) SINGLE=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and not x.get('eval-only', False)]))" | score_matrix single) - EVALS=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix eval) MULTI=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' in x and x.get('scenario-type') != 'agentic-coding' and not x.get('eval-only', False)]))" | score_matrix multi) EVALS=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' not in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix eval) MULTI_EVAL=$(echo "$CONFIG_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps([x for x in d if 'prefill' in x and x.get('scenario-type') != 'agentic-coding' and x.get('run-eval', False)]))" | score_matrix multi-eval) diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index 1d0d7a033a..d4796cc2ec 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2780,7 +2780,7 @@ def test_trusted_changelog_matrix_keeps_multinode_agentic_evals() -> None: assert '"multinode_agentic_evals"' in flatten_command get_jobs_command = get_jobs["run"] - assert "EVALS=$(" in get_jobs_command + assert get_jobs_command.count("EVALS=$(") == 1 assert "score_matrix eval" in get_jobs_command From 3f8eaf902aee2b2f8af68c350db01f6d1db09f86 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:16:33 -0500 Subject: [PATCH 97/99] fix: preserve agentic eval topology settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留 agentic 评估任务的完整拓扑配置 --- .github/workflows/e2e-tests.yml | 5 +++++ utils/evals/test_run_eval_dispatch.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0b5b1e3113..5167e19f33 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -485,7 +485,12 @@ jobs: model-prefix: ${{ matrix.config.model-prefix }} framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} + router: ${{ matrix.config.router && toJson(matrix.config.router) || '' }} + kv-p2p-transfer: ${{ matrix.config['kv-p2p-transfer'] || '' }} tp: ${{ matrix.config.tp }} + pp: ${{ matrix.config.pp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} diff --git a/utils/evals/test_run_eval_dispatch.py b/utils/evals/test_run_eval_dispatch.py index d4796cc2ec..af762d5d92 100644 --- a/utils/evals/test_run_eval_dispatch.py +++ b/utils/evals/test_run_eval_dispatch.py @@ -2685,6 +2685,20 @@ def test_multinode_agentic_waits_only_for_eval_openai_endpoint( def test_agentic_eval_workflow_forwards_runner_contract() -> None: workflow = yaml.safe_load(E2E_WORKFLOW.read_text()) forwarded = workflow["jobs"]["test-sweep-agentic-evals"]["with"] + throughput = workflow["jobs"]["test-sweep-agentic"]["with"] + + for field in ( + "router", + "kv-p2p-transfer", + "tp", + "pp", + "dcp-size", + "pcp-size", + "ep", + "dp-attn", + ): + assert forwarded[field] == throughput[field] + assert forwarded["spec-decoding"] == "${{ matrix.config.spec-decoding }}" assert forwarded["eval-framework"] == AUTO_EVAL_FRAMEWORK_EXPR From f6726219f462ac5019332aa0b0cdc7e6cf5a2049 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:44:18 -0500 Subject: [PATCH 98/99] fix: preserve GB200 synthetic acceptance dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:保留 GB200 吞吐量测试的合成 acceptance 调度 --- runners/launch_gb200-nv.sh | 11 +++++------ runners/test_slurm_utils.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/runners/launch_gb200-nv.sh b/runners/launch_gb200-nv.sh index 0cdb570d3e..9799aa3b71 100755 --- a/runners/launch_gb200-nv.sh +++ b/runners/launch_gb200-nv.sh @@ -711,12 +711,11 @@ if command -v squeue >/dev/null 2>&1; then fi sed -i "s/^name:.*/name: \"${SRT_SLURM_JOB_NAME}\"/" "$CONFIG_PATH" -# Restore real acceptance only for eval jobs. Throughput recipe rendering remains -# unchanged by the opt-in evaluator path. -if [[ "${EVAL_ONLY:-false}" == "true" ]]; then - python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ - "$CONFIG_PATH" "$FRAMEWORK" || exit 1 -fi +# The driver preserves both contracts: real verification for EVAL_ONLY and +# synthetic acceptance for throughput when SYNTHETIC_ACCEPTANCE is enabled. +# It is otherwise a no-op. +python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py" \ + "$CONFIG_PATH" "$FRAMEWORK" || exit 1 # Don't leak the login-node venv to the compute-node orchestrator. sbatch's # default --export=ALL propagates VIRTUAL_ENV (set by `source diff --git a/runners/test_slurm_utils.py b/runners/test_slurm_utils.py index cfd80439e1..162acab380 100644 --- a/runners/test_slurm_utils.py +++ b/runners/test_slurm_utils.py @@ -464,6 +464,18 @@ def test_nvidia_srt_launchers_prepare_kimi_eval_dispatch() -> None: assert 'EVAL_FRAMEWORK:-lm-eval}" != "lm-eval"' in content assert "inject_synthetic_acceptance" in content +def test_gb200_acceptance_driver_is_unconditional_and_fail_closed() -> None: + content = (REPO_ROOT / "runners/launch_gb200-nv.sh").read_text() + command = ( + 'python3 "$GITHUB_WORKSPACE/runners/inject_synthetic_acceptance.py"' + ) + command_index = content.index(command) + + assert content.rfind("\nfi", 0, command_index) > content.rfind( + "\nif ", 0, command_index + ) + assert "|| exit 1" in content[command_index : command_index + 180] + def test_gb200_kimi_compilation_config_preserves_all_settings() -> None: recipes = { From 1cf10bf9bbd750fddd9db2bb5dbcded8fed613cd Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:44:25 -0500 Subject: [PATCH 99/99] docs: remove unnecessary PR waiver files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:删除本 PR 不再需要的补丁豁免文件 --- docs/waiver/2634.md | 56 ------------------------------------------ docs/waiver/2634_zh.md | 56 ------------------------------------------ 2 files changed, 112 deletions(-) delete mode 100644 docs/waiver/2634.md delete mode 100644 docs/waiver/2634_zh.md diff --git a/docs/waiver/2634.md b/docs/waiver/2634.md deleted file mode 100644 index ddf25a4ebb..0000000000 --- a/docs/waiver/2634.md +++ /dev/null @@ -1,56 +0,0 @@ -English | [中文](2634_zh.md) - -# Inference-engine patch waiver for PR 2634 - -Filed under the inference-engine and serving-stack patch item in [`docs/PR_REVIEW_CHECKLIST.md`](../PR_REVIEW_CHECKLIST.md). PR 2634 applies four exact, fail-closed rewrites to pinned upstream installations before serving or evaluation. The rewrites are required for the opt-in Kimi K3 and MiniMax M3 tool-use evaluations described below. - -## Configurations covered - -| Patch | Configurations | Pinned upstream | -|---|---|---| -| vLLM SimpleCPUOffload layer regions | `minimaxm3-fp4-b200-vllm-agentic-mtp`, `minimaxm3-fp4-b300-vllm-agentic-mtp` | `vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45` | -| TensorRT-LLM `store=false` request field | `minimaxm3-fp4-b200-trtllm-agentic-mtp`, `minimaxm3-fp4-b300-trtllm-agentic-mtp` | `nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc23.post1` | -| srt-slurm external DP rank allocation | `kimik3-fp4-gb200-dynamo-vllm-agentic` | NVIDIA/srt-slurm `v1.0.53`, commit `217f94387abeddfed7149a71955dc523e07cd765` | -| srt-slurm provider-eval dispatch | Opt-in `kimi-vendor`, `minimax-vendor`, and `bfcl` eval-only rows launched through the NVIDIA srt-slurm paths | Each launcher's checked and commit-verified srt-slurm revision | - -Patch entrypoints are [`runners/patch_vllm_simple_kv_offload.py`](../../runners/patch_vllm_simple_kv_offload.py), [`runners/patch_trtllm_chat_store.py`](../../runners/patch_trtllm_chat_store.py), [`runners/patch_srt_vllm_dp_ranks.py`](../../runners/patch_srt_vllm_dp_ranks.py), and [`runners/patch_srt_eval_dispatch.py`](../../runners/patch_srt_eval_dispatch.py). - -## Exact changes and necessity - -### vLLM SimpleCPUOffload - -MiniMax M3 uses heterogeneous KDA and MLA KV layouts. The pinned vLLM worker treats the first logical allocation size as one common backing region and attempts to reshape every layer through that size. The resulting tensor view is invalid for layers with a different physical block stride. The patch detects heterogeneous storage, slices each layer from its real storage offset, validates the required region size, and keeps the original shared-storage behavior for homogeneous layouts. - -The stock image fails before serving offloaded MiniMax M3 rows. The patched path produced complete BFCL artifacts with score `1.0` on B200 in run [33328257980](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33328257980) and on B300 in run [33330971674](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33330971674). vLLM issue [41830](https://github.com/vllm-project/vllm/issues/41830) documents the related unsupported interaction between SimpleCPUOffload and heterogeneous or hybrid KV cache management. No released upstream fix matches this exact layer-region rewrite yet. - -### TensorRT-LLM chat request compatibility - -BFCL's stock OpenAI client sends the standard optional `store=false` field. TensorRT-LLM `1.3.0rc23.post1` rejects the request with HTTP 400 because its `ChatCompletionRequest` model does not declare the field. The patch adds only `store: Literal[False] = False`, so persistence requests remain rejected while the non-persistent standard request is accepted. Run [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) completed all four BFCL smoke requests through the patched B200 TensorRT-LLM endpoint. The resulting `0.25` score is model quality, not request compatibility. Upstream issue [9709](https://github.com/NVIDIA/TensorRT-LLM/issues/9709) tracks the same request-model extension mechanism for missing OpenAI-compatible fields; no dedicated upstream `store` issue exists yet. - -### srt-slurm external DP rank allocation - -srt-slurm `v1.0.53` creates one process per GPU for external DP, even when each DP rank owns `TP × PP` GPUs. Kimi K3 TP4 and DP4 therefore receives sixteen one-GPU ranks instead of four four-GPU ranks. The patch validates `DP × TP × PP` against the allocation and creates one process with the complete GPU set for each DP rank. GB200 run [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) produced complete BFCL artifacts with score `1.0` through that topology. - -NVIDIA/srt-slurm [PR 331](https://github.com/NVIDIA/srt-slurm/pull/331), released in `v1.0.82`, added a per-node external-DP implementation with combined data and model parallelism. PR 2634 retains the hardware-validated `v1.0.53` pin until the Kimi recipe is rendered and exercised against the newer release. - -### srt-slurm provider-eval dispatch - -The pinned srt-slurm revisions hardcode the `lm-eval` runner and forward only the legacy eval environment. The opt-in provider suites require `EVAL_FRAMEWORK`, `EVAL_SUITE`, `EVAL_LIMIT`, and their native artifact families. The patch extends only the eval environment whitelist and eval command. Throughput jobs do not activate it, and every replacement is exact-count and fail-closed. - -The merged upstream lm-eval implementation came through NVIDIA/srt-slurm [PR 122](https://github.com/NVIDIA/srt-slurm/pull/122). The latest `v1.0.82` still constructs the post-eval runner with `get_runner("lm-eval")`, so upgrading alone does not provide provider dispatch. Open upstream [PR 41](https://github.com/NVIDIA/srt-slurm/pull/41) is the tracked generalization path for external or custom eval harnesses. - -## Safety controls - -- Every patch matches one exact pristine source block and fails on missing, duplicate, or partially patched state. -- Reapplying a complete patch is a no-op. -- Engine patches are restricted to the named MiniMax configurations. The eval-dispatch patch runs only when `EVAL_FRAMEWORK` is not `lm-eval`. -- Eval-only acceptance rewrites are gated by `EVAL_ONLY=true`; default throughput recipe rendering is unchanged. -- Unit tests cover pristine, idempotent, partial, and unsupported source states. - -## Removal plan - -1. Remove `patch_vllm_simple_kv_offload.py` when an official vLLM image correctly allocates heterogeneous SimpleCPUOffload layer regions. Re-run the B200 and B300 DRAM-offload rows against the stock image before deleting the patch and this waiver section. -2. Remove `patch_trtllm_chat_store.py` when the pinned TensorRT-LLM release accepts the standard non-persistent `store=false` field. Re-run BFCL smoke on B200 and B300 without the patch. -3. Migrate the Kimi GB200 recipes to srt-slurm `v1.0.82` or newer, verify rendered DP4 and TP4 commands and hardware artifacts, then delete `patch_srt_vllm_dp_ranks.py`. -4. Remove `patch_srt_eval_dispatch.py` when the pinned srt-slurm release supports selecting external eval runners and staging their native artifacts without a source rewrite. -5. Delete this waiver in the same PR that removes the final covered runtime patch. diff --git a/docs/waiver/2634_zh.md b/docs/waiver/2634_zh.md deleted file mode 100644 index 0731d86b48..0000000000 --- a/docs/waiver/2634_zh.md +++ /dev/null @@ -1,56 +0,0 @@ -[English](2634.md) | 中文 - -# PR 2634 推理引擎补丁豁免说明 - -本文档对应 [`docs/PR_REVIEW_CHECKLIST.md`](../PR_REVIEW_CHECKLIST.md) 中关于推理引擎与服务栈补丁的检查项。PR 2634 在启动服务或评估之前,对固定版本的上游安装执行四项精确、失败即终止的源码改写。这些改写用于下文所列的 Kimi K3 和 MiniMax M3 工具调用评估,且仅在显式启用时生效。 - -## 适用配置 - -| 补丁 | 配置 | 固定的上游版本 | -|---|---|---| -| vLLM SimpleCPUOffload 分层存储区域 | `minimaxm3-fp4-b200-vllm-agentic-mtp`、`minimaxm3-fp4-b300-vllm-agentic-mtp` | `vllm/vllm-openai:nightly-1dc464d42681d22f38caf1fdc1eb632dc4421c45` | -| TensorRT-LLM `store=false` 请求字段 | `minimaxm3-fp4-b200-trtllm-agentic-mtp`、`minimaxm3-fp4-b300-trtllm-agentic-mtp` | `nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc23.post1` | -| srt-slurm 外部数据并行(DP) rank 分配 | `kimik3-fp4-gb200-dynamo-vllm-agentic` | NVIDIA/srt-slurm `v1.0.53`,commit `217f94387abeddfed7149a71955dc523e07cd765` | -| srt-slurm provider 评估调度 | 通过 NVIDIA srt-slurm 路径启动的 `kimi-vendor`、`minimax-vendor` 和 `bfcl` eval-only 行 | 各启动器经过校验且固定 commit 的 srt-slurm 版本 | - -补丁入口分别为 [`runners/patch_vllm_simple_kv_offload.py`](../../runners/patch_vllm_simple_kv_offload.py)、[`runners/patch_trtllm_chat_store.py`](../../runners/patch_trtllm_chat_store.py)、[`runners/patch_srt_vllm_dp_ranks.py`](../../runners/patch_srt_vllm_dp_ranks.py) 和 [`runners/patch_srt_eval_dispatch.py`](../../runners/patch_srt_eval_dispatch.py)。 - -## 具体改动与必要性 - -### vLLM SimpleCPUOffload - -MiniMax M3 使用异构的 KDA 与 MLA KV 布局。固定版本的 vLLM worker 把第一段逻辑分配大小视为统一的后备存储区域,并按该大小重排所有层。对于物理 block 步长不同的层,这会产生非法 tensor view。补丁会检测异构存储,按真实存储偏移切分各层,校验所需区域大小,同时保留同构布局原有的共享存储逻辑。 - -原始镜像会在 DRAM offload 的 MiniMax M3 配置开始服务前失败。应用补丁后,B200 的运行 [33328257980](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33328257980) 和 B300 的运行 [33330971674](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33330971674) 均生成了完整 BFCL 产物,得分为 `1.0`。vLLM issue [41830](https://github.com/vllm-project/vllm/issues/41830) 记录了 SimpleCPUOffload 与异构或混合 KV cache 管理之间的相关兼容性问题。目前尚无已发布的上游修复能替代这项分层存储区域改写。 - -### TensorRT-LLM chat 请求兼容性 - -BFCL 原生 OpenAI 客户端会发送标准可选字段 `store=false`。TensorRT-LLM `1.3.0rc23.post1` 的 `ChatCompletionRequest` 模型未声明该字段,因此以 HTTP 400 拒绝请求。补丁仅添加 `store: Literal[False] = False`,仍会拒绝要求持久化的请求,只接受标准的非持久化请求。运行 [33326709108](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33326709108) 通过应用补丁的 B200 TensorRT-LLM endpoint 完成了全部四个 BFCL smoke 请求。最终 `0.25` 得分属于模型质量问题,而非请求兼容性问题。上游 issue [9709](https://github.com/NVIDIA/TensorRT-LLM/issues/9709) 使用相同的请求模型扩展机制处理缺失的 OpenAI 兼容字段;目前尚无专门跟踪 `store` 字段的上游 issue。 - -### srt-slurm 外部 DP rank 分配 - -srt-slurm `v1.0.53` 在外部 DP 模式下为每张 GPU 创建一个进程,即使每个 DP rank 应占用 `TP × PP` 张 GPU。Kimi K3 的 TP4 与 DP4 因此会错误地得到十六个单 GPU rank,而不是四个四 GPU rank。补丁会校验 `DP × TP × PP` 与分配资源一致,并为每个 DP rank 创建一个持有完整 GPU 集合的进程。GB200 运行 [33304519247](https://github.com/SemiAnalysisAI/InferenceX/actions/runs/33304519247) 在该拓扑上生成了完整 BFCL 产物,得分为 `1.0`。 - -NVIDIA/srt-slurm [PR 331](https://github.com/NVIDIA/srt-slurm/pull/331) 已随 `v1.0.82` 发布,其中新增了支持数据并行与模型并行组合的 per-node 外部 DP 实现。PR 2634 暂时保留经过硬件验证的 `v1.0.53` 固定版本,待 Kimi recipe 使用新版本完成渲染检查与硬件验证后迁移。 - -### srt-slurm provider 评估调度 - -固定的 srt-slurm 版本只支持硬编码的 `lm-eval` runner,并且仅传递旧版评估环境变量。显式启用的 provider 评估需要 `EVAL_FRAMEWORK`、`EVAL_SUITE`、`EVAL_LIMIT` 以及各自的原生产物。补丁只扩展评估环境变量白名单和评估命令。吞吐量任务不会触发这些逻辑,所有替换都要求精确匹配次数,否则立即失败。 - -上游合并的 lm-eval 实现来自 NVIDIA/srt-slurm [PR 122](https://github.com/NVIDIA/srt-slurm/pull/122)。最新 `v1.0.82` 仍使用 `get_runner("lm-eval")` 构建后置评估 runner,因此仅升级版本无法提供 provider 调度能力。上游仍在开放的 [PR 41](https://github.com/NVIDIA/srt-slurm/pull/41) 是支持外部或自定义评估 harness 的通用化跟踪路径。 - -## 安全措施 - -- 每项补丁只匹配一段完全一致的原始源码。目标缺失、重复或处于部分补丁状态时都会失败。 -- 对已完整应用补丁的源码重复执行时不会产生改动。 -- 推理引擎补丁仅用于上述 MiniMax 配置。仅当 `EVAL_FRAMEWORK` 不为 `lm-eval` 时才应用评估调度补丁。 -- 评估专用的 acceptance 改写受 `EVAL_ONLY=true` 控制,默认吞吐量 recipe 渲染不变。 -- 单元测试覆盖原始、幂等、部分补丁和不受支持的源码状态。 - -## 移除计划 - -1. 当官方 vLLM 镜像能够正确分配异构 SimpleCPUOffload 分层存储区域时,使用原始镜像重新运行 B200 和 B300 DRAM offload 配置,验证通过后删除 `patch_vllm_simple_kv_offload.py` 及本豁免中的对应章节。 -2. 当固定版本的 TensorRT-LLM 支持标准非持久化字段 `store=false` 时,在不应用补丁的情况下重新运行 B200 和 B300 BFCL smoke,随后删除 `patch_trtllm_chat_store.py`。 -3. 将 Kimi GB200 recipe 升级至 srt-slurm `v1.0.82` 或更高版本,验证渲染出的 DP4 与 TP4 命令及硬件产物,随后删除 `patch_srt_vllm_dp_ranks.py`。 -4. 当固定版本的 srt-slurm 能够选择外部评估 runner,并在无需源码改写的情况下归档其原生产物时,删除 `patch_srt_eval_dispatch.py`。 -5. 在移除最后一项受本文件覆盖的运行时补丁时,同步删除本豁免文档。