Skip to content

Commit 476d0d2

Browse files
authored
Fix agentic SWE-bench eval dispatch: dedicated agentic_evals bucket + sweep job / 修复 agentic SWE-bench 评估调度:新增 agentic_evals 独立分桶与扫描任务 (#2267)
* fix: dispatch agentic SWE-bench eval rows via a dedicated bucket and sweep job Since #1947, generate_sweep_configs --evals-only marks the top-concurrency agentic arm of each config as a SWE-bench eval row, but two consumers were never taught about agentic eval rows, so any PR touching a single-node agentic-coding config failed its sweep: 1. Schema: ChangelogMatrixEntry.evals only accepted SingleNodeMatrixEntry (fixed-seq-len), so process_changelog.py's final model_validate rejected the agentic row and check-changelog failed before any GPU work. 2. Dispatch: even with the schema widened, run-sweep.yml's sweep-evals job dispatches every eval row with fixed-seq-len inputs; an agentic row gets empty ISL/OSL/MAX_MODEL_LEN and scenario-type defaults to fixed-seq-len, so the launcher runs the fixed_seq_len benchmark script, which exits on check_env_vars. Fix: route agentic eval rows into a new agentic_evals bucket (mirroring the single_node['agentic'] design) dispatched by a new sweep-agentic-evals job cloned from sweep-agentic's input block plus run-eval/eval-only, so an agentic row can never reach the fixed-seq-len eval dispatch. The agentic model accepts optional run-eval/eval-only carried by eval rows; exclude_none keeps benchmark row output byte-identical. 中文:自 #1947 起,generate_sweep_configs --evals-only 会将每个配置中并发度最高的 agentic 搜索空间条目标记为 SWE-bench 评估行,但下游两个消费方并不认识 agentic 评估行,导致任何涉及单节点 agentic-coding 配置的 PR 扫描失败:其一,schema 层 ChangelogMatrixEntry.evals 仅接受固定序列长度条目,check-changelog 直接报错; 其二,调度层 sweep-evals 以固定序列长度的输入派发评估行,ISL/OSL/MAX_MODEL_LEN 为空且 scenario-type 回落为 fixed-seq-len,启动器因此运行错误的基准测试脚本并 在 check_env_vars 处退出。本修复新增 agentic_evals 独立分桶(与 single_node['agentic'] 设计一致),由新的 sweep-agentic-evals 任务按 sweep-agentic 的输入派发并附加 run-eval/eval-only,使 agentic 评估行不可能进入 固定序列长度的评估调度路径;基准测试行的输出保持逐字节不变。 * test: trim agentic eval bucket tests to essential coverage Collapse fixtures into module constants, merge the accept/reject checks into one test, and drop redundant cases; added test lines go from 212 to 100 with the two core guards kept (agentic rows validate only in agentic_evals; realistic rows split end-to-end through the real schema). 中文:精简 agentic 评估分桶测试——fixture 合并为模块常量,接受/拒绝检查合并 为单个测试并移除冗余用例,新增测试行数从 212 降至 100,保留两项核心守卫 (agentic 行仅能通过 agentic_evals 校验;真实行形状端到端经过真实 schema 拆分)。 * chore: add changelog entry to validate agentic eval dispatch end-to-end Eval-only, agentic-coding-scoped entry for dsv4-fp4-b300-vllm-agentic so the PR sweep (and the on-push main sweep after merge) runs exactly one job: the DEP8 top-concurrency SWE-bench eval through the new sweep-agentic-evals dispatch, confirming the fix for the eval-only failures first seen on #2258/#2259. 中文:新增 changelog 条目以端到端验证 agentic 评估调度——针对 dsv4-fp4-b300-vllm-agentic 的 evals-only、仅 agentic-coding 场景条目,使 PR 扫描(及合并后 main 分支的 push 触发扫描)只运行一个任务:DEP8 最高并发 的 SWE-bench 评估经由新的 sweep-agentic-evals 调度执行,确认对 #2258/#2259 上首次出现的 eval-only 失败的修复。 * ws
1 parent 0094291 commit 476d0d2

6 files changed

Lines changed: 179 additions & 3 deletions

File tree

.github/workflows/run-sweep.yml

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,58 @@ jobs:
628628
run-eval: true
629629
eval-only: true
630630

631+
# Agentic (SWE-bench) eval rows carry the agentic input shape, so they are
632+
# dispatched with sweep-agentic's inputs rather than sweep-evals' fixed-seq-len
633+
# inputs (isl/osl/max-model-len, which agentic rows don't have).
634+
sweep-agentic-evals:
635+
needs: [setup, canary-select, canary-sweep]
636+
if: >-
637+
${{
638+
!cancelled() &&
639+
needs.setup.result == 'success' &&
640+
needs.setup.outputs.reuse-enabled != 'true' &&
641+
(needs.canary-sweep.result == 'success' || needs.canary-sweep.result == 'skipped') &&
642+
toJson(fromJson(needs.setup.outputs.search-space-config).agentic_evals) != '[]' &&
643+
toJson(fromJson(needs.setup.outputs.search-space-config).agentic_evals) != 'null'
644+
}}
645+
uses: ./.github/workflows/benchmark-tmpl.yml
646+
name: agentic eval /
647+
strategy:
648+
fail-fast: ${{ contains(github.event.pull_request.labels.*.name, 'full-sweep-fail-fast') || contains(github.event.pull_request.labels.*.name, 'full-sweep-fail-fast-no-canary') }}
649+
matrix:
650+
config: ${{ fromJson(needs.setup.outputs.search-space-config).agentic_evals }}
651+
secrets: inherit
652+
with:
653+
exp-name: ${{ matrix.config.exp-name }}
654+
runner: ${{ matrix.config.runner }}
655+
image: ${{ matrix.config.image }}
656+
model: ${{ matrix.config.model }}
657+
model-prefix: ${{ matrix.config.model-prefix }}
658+
framework: ${{ matrix.config.framework }}
659+
precision: ${{ matrix.config.precision }}
660+
router: ${{ matrix.config.router && toJson(matrix.config.router) || '' }}
661+
kv-p2p-transfer: ${{ matrix.config['kv-p2p-transfer'] || '' }}
662+
tp: ${{ matrix.config.tp }}
663+
pp: ${{ matrix.config.pp }}
664+
dcp-size: ${{ matrix.config.dcp-size }}
665+
pcp-size: ${{ matrix.config.pcp-size }}
666+
ep: ${{ matrix.config.ep }}
667+
dp-attn: ${{ matrix.config.dp-attn }}
668+
conc: ${{ matrix.config.conc }}
669+
kv-offloading: ${{ matrix.config.kv-offloading }}
670+
kv-offload-backend: ${{ matrix.config['kv-offload-backend'].name }}
671+
kv-offload-backend-metadata: ${{ matrix.config['kv-offload-backend'] && toJson(matrix.config['kv-offload-backend']) || '' }}
672+
total-cpu-dram-gb: ${{ matrix.config.total-cpu-dram-gb }}
673+
duration: ${{ matrix.config.duration }}
674+
isl: '0'
675+
osl: '0'
676+
max-model-len: '0'
677+
spec-decoding: ${{ matrix.config.spec-decoding }}
678+
disagg: ${{ 'false' }}
679+
run-eval: true
680+
eval-only: true
681+
scenario-type: agentic-coding
682+
631683
sweep-multi-node-evals:
632684
needs: [setup, canary-select, canary-sweep]
633685
if: >-
@@ -714,8 +766,8 @@ jobs:
714766
result-prefix: "bmk"
715767

716768
collect-evals:
717-
needs: [sweep-evals, sweep-multi-node-evals, setup]
718-
if: ${{ always() && needs.setup.result != 'skipped' && (needs.sweep-evals.result != 'skipped' || needs.sweep-multi-node-evals.result != 'skipped') }}
769+
needs: [sweep-evals, sweep-agentic-evals, sweep-multi-node-evals, setup]
770+
if: ${{ always() && needs.setup.result != 'skipped' && (needs.sweep-evals.result != 'skipped' || needs.sweep-agentic-evals.result != 'skipped' || needs.sweep-multi-node-evals.result != 'skipped') }}
719771
uses: ./.github/workflows/collect-evals.yml
720772
secrets: inherit
721773

utils/matrix_logic/test_validation.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SingleNodeMasterConfigEntry,
1919
MultiNodeMasterConfigEntry,
2020
ChangelogEntry,
21+
ChangelogMatrixEntry,
2122
validate_matrix_entry,
2223
validate_master_config,
2324
validate_runner_config,
@@ -1400,6 +1401,49 @@ def test_scenario_type_must_be_nonempty_and_supported(self, scenario_type):
14001401
})
14011402

14021403

1404+
# =============================================================================
1405+
# Test ChangelogMatrixEntry
1406+
# =============================================================================
1407+
1408+
AGENTIC_EVAL_ROW = {
1409+
"image": "vllm/vllm-openai:nightly", "model": "deepseek-ai/DeepSeek-V4-Pro",
1410+
"model-prefix": "dsv4", "precision": "fp4", "framework": "vllm",
1411+
"runner": "cluster:b300-nv", "tp": 8, "pp": 1, "dcp-size": 1,
1412+
"pcp-size": 1, "ep": 8, "dp-attn": True, "spec-decoding": "mtp",
1413+
"conc": 224, "kv-offloading": "none", "total-cpu-dram-gb": 0,
1414+
"duration": 3600, "exp-name": "dsv4_tp8_conc224_kvnone_spec-mtp",
1415+
"scenario-type": "agentic-coding", "run-eval": True, "eval-only": True,
1416+
}
1417+
1418+
CHANGELOG_METADATA = {
1419+
"base_ref": "base", "head_ref": "head",
1420+
"entries": [{
1421+
"config-keys": ["test-config"], "description": ["Test entry"],
1422+
"pr-link": "https://github.com/SemiAnalysisAI/InferenceX/pull/1",
1423+
}],
1424+
}
1425+
1426+
1427+
class TestChangelogMatrixEntry:
1428+
"""Tests for the final search-space contract consumed by run-sweep.yml."""
1429+
1430+
def test_agentic_eval_rows_live_in_agentic_evals_only(self):
1431+
"""`evals` is dispatched with fixed-seq-len inputs (isl/osl/
1432+
max-model-len), so agentic rows must only validate in agentic_evals."""
1433+
entry = ChangelogMatrixEntry.model_validate({
1434+
"agentic_evals": [AGENTIC_EVAL_ROW],
1435+
"changelog_metadata": CHANGELOG_METADATA,
1436+
})
1437+
assert entry.agentic_evals[0].run_eval is True
1438+
assert entry.agentic_evals[0].eval_only is True
1439+
1440+
with pytest.raises(ValueError):
1441+
ChangelogMatrixEntry.model_validate({
1442+
"evals": [AGENTIC_EVAL_ROW],
1443+
"changelog_metadata": CHANGELOG_METADATA,
1444+
})
1445+
1446+
14031447
# =============================================================================
14041448
# Test load_config_files
14051449
# =============================================================================

utils/matrix_logic/validation.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ class SingleNodeAgenticMatrixEntry(BaseModel):
291291
duration: int = Field(alias=Fields.DURATION.value)
292292
exp_name: str = Field(alias=Fields.EXP_NAME.value)
293293
scenario_type: str = Field(alias=Fields.SCENARIO_TYPE.value)
294+
# Agentic eval rows (SWE-bench) carry run-eval/eval-only; benchmark rows
295+
# omit them, and exclude_none keeps them out of dumped benchmark output.
296+
run_eval: Optional[bool] = Field(default=None, alias=Fields.RUN_EVAL.value)
297+
eval_only: Optional[bool] = Field(default=None, alias=Fields.EVAL_ONLY.value)
294298

295299
@model_validator(mode='after')
296300
def validate_kv_offload_fields(self):
@@ -890,6 +894,13 @@ class ChangelogMatrixEntry(BaseModel):
890894
multi_node: dict[str, list[Union[MultiNodeMatrixEntry, MultiNodeAgenticMatrixEntry]]
891895
] = Field(default_factory=dict)
892896
evals: list[SingleNodeMatrixEntry] = Field(default_factory=list)
897+
# Agentic (SWE-bench) eval rows live in their own bucket rather than a
898+
# union inside `evals`: each bucket maps 1:1 to a run-sweep.yml job with a
899+
# static input block, so an agentic row can never reach the fixed-seq-len
900+
# eval dispatch (which reads isl/osl/max-model-len and would launch the
901+
# wrong benchmark script).
902+
agentic_evals: list[SingleNodeAgenticMatrixEntry] = Field(
903+
default_factory=list)
893904
multinode_evals: list[MultiNodeMatrixEntry] = Field(default_factory=list)
894905
changelog_metadata: ChangelogMetadata
895906

utils/process_changelog.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def main():
137137
"single_node": defaultdict(list),
138138
"multi_node": defaultdict(list),
139139
"evals": [],
140+
"agentic_evals": [],
140141
"multinode_evals": [],
141142
"changelog_metadata": {
142143
"base_ref": args.base_ref,
@@ -271,7 +272,18 @@ def main():
271272
seq_len_str = seq_len_to_str(result["isl"], result["osl"])
272273
final_results["single_node"][seq_len_str].append(result)
273274

274-
final_results["evals"] = [e for e in all_eval_results if e.get("prefill") is None]
275+
# Agentic eval rows go to their own bucket so run-sweep.yml can dispatch
276+
# them with agentic inputs (scenario-type, kv-offloading, ...) instead of
277+
# the fixed-seq-len inputs (isl/osl/max-model-len) they don't have.
278+
single_node_evals = [e for e in all_eval_results if e.get("prefill") is None]
279+
final_results["evals"] = [
280+
e for e in single_node_evals
281+
if e.get("scenario-type") != "agentic-coding"
282+
]
283+
final_results["agentic_evals"] = [
284+
e for e in single_node_evals
285+
if e.get("scenario-type") == "agentic-coding"
286+
]
275287
final_results["multinode_evals"] = [e for e in all_eval_results if e.get("prefill") is not None]
276288

277289
# Validate final results structure

utils/recover_failed_ingest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ def build_config(
492492
)
493493
+ len(config.get("multi_node", {}).get("agentic", []) or []),
494494
"eval_jobs": len(config.get("evals", []) or [])
495+
+ len(config.get("agentic_evals", []) or [])
495496
+ len(config.get("multinode_evals", []) or []),
496497
}
497498

utils/test_process_changelog.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,3 +583,59 @@ def fake_run(command, **kwargs):
583583
assert "--all-evals" not in commands[2]
584584
assert _scenario_values(commands[2]) == ["fixed-seq-len"]
585585
json.loads(capsys.readouterr().out)
586+
587+
588+
def test_eval_rows_split_into_fixed_and_agentic_buckets(
589+
monkeypatch,
590+
capsys,
591+
):
592+
"""Realistic eval rows must pass final validation and land in the bucket
593+
matching their dispatch job: fixed-seq-len rows in `evals`, agentic
594+
(SWE-bench) rows in `agentic_evals`."""
595+
added_yaml = """
596+
- config-keys:
597+
- test-config
598+
description:
599+
- Mixed fixed-seq-len and agentic eval selection
600+
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/1
601+
"""
602+
common = {
603+
"image": "vllm/vllm-openai:v0.11.0",
604+
"model": "deepseek-ai/DeepSeek-V4-Pro", "model-prefix": "dsv4",
605+
"precision": "fp4", "framework": "vllm", "spec-decoding": "mtp",
606+
"runner": "cluster:b300-nv", "tp": 8, "pp": 1, "dcp-size": 1,
607+
"pcp-size": 1, "ep": 8, "dp-attn": True, "conc": 224,
608+
"run-eval": True, "eval-only": True,
609+
}
610+
fixed_eval_row = {
611+
**common, "isl": 8192, "osl": 1024, "max-model-len": 10240,
612+
"disagg": False, "exp-name": "fixed_eval",
613+
}
614+
agentic_eval_row = {
615+
**common, "kv-offloading": "none", "total-cpu-dram-gb": 0,
616+
"duration": 3600, "scenario-type": "agentic-coding",
617+
"exp-name": "agentic_eval",
618+
}
619+
620+
monkeypatch.setattr(
621+
process_changelog, "get_added_lines", lambda *_: added_yaml)
622+
monkeypatch.setattr(
623+
process_changelog, "load_config_files", lambda _: {"test-config": {}})
624+
625+
def fake_run(command, **kwargs):
626+
is_evals = "--evals-only" in command
627+
rows = [fixed_eval_row, agentic_eval_row] if is_evals else []
628+
return SimpleNamespace(stdout=json.dumps(rows))
629+
630+
monkeypatch.setattr(subprocess, "run", fake_run)
631+
monkeypatch.setattr(sys, "argv", [
632+
"process_changelog.py", "--base-ref", "base", "--head-ref", "head",
633+
"--changelog-file", "perf-changelog.yaml",
634+
])
635+
636+
process_changelog.main()
637+
638+
output = json.loads(capsys.readouterr().out)
639+
assert [r["exp-name"] for r in output["evals"]] == ["fixed_eval"]
640+
assert [r["exp-name"] for r in output["agentic_evals"]] == ["agentic_eval"]
641+
assert output["multinode_evals"] == []

0 commit comments

Comments
 (0)