docs: add resource prep section and sync docker/bash helpers
- Rewrite myread.md with proper markdown and four download sources: code, data, evalscope-complete-py312 image, execution images. - Add bash/collect_results.py, perf_backup.py, pull_swe_bench_images.py for result aggregation, breakpoint perf recovery, and SWE-bench image pre-pulling. - Update bash/run.py with multi-run suites, perf backup/restore, and whitelist-based summary. - Update config/dpv4-int8_nothinking.yaml benchmark parameters. - Ignore /docker_images and /results in .gitignore.
This commit is contained in:
parent
03b49a39d0
commit
2ee0af0728
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,7 +19,8 @@ env/
|
|||||||
/outputs/
|
/outputs/
|
||||||
/output/
|
/output/
|
||||||
/output*
|
/output*
|
||||||
|
/docker_images
|
||||||
|
/results
|
||||||
/tau2-bench
|
/tau2-bench
|
||||||
/log
|
/log
|
||||||
/docker
|
/docker
|
||||||
|
|||||||
631
bash/collect_results.py
Normal file
631
bash/collect_results.py
Normal file
@ -0,0 +1,631 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Collect benchmark results from an EvalScope output directory and write a summary
|
||||||
|
Excel/CSV similar to:
|
||||||
|
|
||||||
|
P800模型能力评测结果 - DS4-Flash-INT8-NO-Thinking-2.0-FULL.csv
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Scores are averaged across seeds / multi-runs.
|
||||||
|
- Perf metrics (latency, TTFT, TPOT, TPS, tokens) are recomputed from raw
|
||||||
|
predictions across all seeds / multi-runs, so resuming from a checkpoint
|
||||||
|
no longer resets cumulative statistics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import statistics
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
# Map benchmark -> capability domain (same as the reference CSV)
|
||||||
|
BENCHMARK_DOMAIN = {
|
||||||
|
'bigcodebench': '代码与工程',
|
||||||
|
'humaneval': '代码与工程',
|
||||||
|
'live_code_bench': '代码与工程',
|
||||||
|
'aime24': '推理与数学',
|
||||||
|
'aime25': '推理与数学',
|
||||||
|
'aime26': '推理与数学',
|
||||||
|
'hmmt26': '推理与数学',
|
||||||
|
'imo_answerbench': '推理与数学',
|
||||||
|
'gsm8k': '推理与数学',
|
||||||
|
'competition_math': '推理与数学',
|
||||||
|
'bbh': '推理与数学',
|
||||||
|
'drop': '推理与数学',
|
||||||
|
'gpqa_diamond': '知识与语言理解',
|
||||||
|
'hle': '知识与语言理解',
|
||||||
|
'hle': '知识与语言理解',
|
||||||
|
'hle_low': '知识与语言理解',
|
||||||
|
'mmlu_pro': '知识与语言理解',
|
||||||
|
'simple_qa': '知识与语言理解',
|
||||||
|
'super_gpqa': '知识与语言理解',
|
||||||
|
'mmlu': '知识与语言理解',
|
||||||
|
'cmmlu': '知识与语言理解',
|
||||||
|
'arc': '知识与语言理解',
|
||||||
|
'hellaswag': '知识与语言理解',
|
||||||
|
'trivia_qa': '知识与语言理解',
|
||||||
|
'winogrande': '知识与语言理解',
|
||||||
|
'longbench_v2': '长上下文',
|
||||||
|
'openai_mrcr': '长上下文',
|
||||||
|
'tau2_bench': '智能体与工具',
|
||||||
|
'general_fc': '智能体与工具',
|
||||||
|
'bfcl_v3': '智能体与工具',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Column order matching the reference CSV
|
||||||
|
OUTPUT_COLUMNS = [
|
||||||
|
'分类',
|
||||||
|
'Benchmark',
|
||||||
|
'得分',
|
||||||
|
'实测时间(h)',
|
||||||
|
'总样本数',
|
||||||
|
'延迟_mean(s)',
|
||||||
|
'输出TPS',
|
||||||
|
'请求QPS',
|
||||||
|
'输入tokens_mean',
|
||||||
|
'输出tokens_mean',
|
||||||
|
'累计总tokens',
|
||||||
|
'TTFT_mean(s)',
|
||||||
|
'TTFT P90',
|
||||||
|
'TTFT P99',
|
||||||
|
'TPOT_mean(s)',
|
||||||
|
'TPOT P90',
|
||||||
|
'TPOT P99',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Some evalscope output directory names do not match the canonical benchmark
|
||||||
|
# name (e.g. the hle benchmark writes to the `hle_low` directory because of
|
||||||
|
# subset naming). Map them to the canonical name here so the summary uses a
|
||||||
|
# consistent label.
|
||||||
|
BENCHMARK_NAME_ALIAS = {
|
||||||
|
'hle_low': 'hle',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values, q):
|
||||||
|
"""Return the q-th percentile using numpy's linear interpolation."""
|
||||||
|
if not values:
|
||||||
|
return np.nan
|
||||||
|
return float(np.percentile(values, q))
|
||||||
|
|
||||||
|
|
||||||
|
def read_predictions(pred_file: Path):
|
||||||
|
"""Yield perf_metrics dicts from a predictions JSONL file."""
|
||||||
|
for obj in read_predictions_with_index(pred_file):
|
||||||
|
if obj['perf_metrics'] is not None:
|
||||||
|
yield obj['perf_metrics']
|
||||||
|
|
||||||
|
|
||||||
|
def read_predictions_with_index(pred_file: Path):
|
||||||
|
"""Yield ``{'index', 'perf_metrics'}`` dicts from a predictions JSONL file."""
|
||||||
|
if not pred_file.exists():
|
||||||
|
return
|
||||||
|
with open(pred_file, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
model_output = obj.get('model_output', {})
|
||||||
|
pm = model_output.get('perf_metrics')
|
||||||
|
if not pm and 'choices' in model_output:
|
||||||
|
choices = model_output['choices']
|
||||||
|
if choices and 'message' in choices[0]:
|
||||||
|
pm = choices[0]['message'].get('perf_metrics')
|
||||||
|
yield {'index': obj.get('index'), 'perf_metrics': pm}
|
||||||
|
|
||||||
|
|
||||||
|
def load_backup_summary(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Load the durable ``perf_stats_backup/<benchmark>__<model>.json``.
|
||||||
|
|
||||||
|
Returns the ``summary`` dict (with ``latency`` / ``ttft`` / ``tpot`` /
|
||||||
|
``throughput`` / ``usage`` sub-dicts) or ``None`` if no backup exists.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from perf_backup import get_backup_paths
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
perf_path, _ = get_backup_paths(Path(output_dir), benchmark, model_name)
|
||||||
|
if not perf_path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
payload = json.loads(perf_path.read_text(encoding='utf-8'))
|
||||||
|
return payload.get('summary')
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_report(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Find the first report JSON for a benchmark/model under output_dir."""
|
||||||
|
bench_dir = output_dir / benchmark
|
||||||
|
if not bench_dir.exists():
|
||||||
|
return None
|
||||||
|
for seed_dir in sorted(bench_dir.iterdir()):
|
||||||
|
if not seed_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for candidate in find_report_candidates(seed_dir, benchmark, model_name):
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_report_candidates(seed_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Return candidate report paths, accepting variant naming patterns."""
|
||||||
|
# The actual report filename may not match the directory name exactly
|
||||||
|
# (e.g. hle_low directory holds hle.json). Try all .json files under
|
||||||
|
# the model directory.
|
||||||
|
candidates = [
|
||||||
|
seed_dir / 'reports' / model_name / f'{benchmark}.json',
|
||||||
|
seed_dir / 'reports' / f'{benchmark}.json',
|
||||||
|
]
|
||||||
|
for p in (seed_dir / 'reports' / model_name).glob('*.json'):
|
||||||
|
candidates.append(p)
|
||||||
|
for p in (seed_dir / 'reports').glob('*.json'):
|
||||||
|
candidates.append(p)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def find_all_reports(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Find all report JSONs for a benchmark/model (across seeds/runs)."""
|
||||||
|
bench_dir = output_dir / benchmark
|
||||||
|
reports = []
|
||||||
|
if not bench_dir.exists():
|
||||||
|
return reports
|
||||||
|
for seed_dir in sorted(bench_dir.iterdir()):
|
||||||
|
if not seed_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for candidate in find_report_candidates(seed_dir, benchmark, model_name):
|
||||||
|
if candidate.exists() and candidate not in reports:
|
||||||
|
reports.append(candidate)
|
||||||
|
break
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
|
def find_archive_predictions(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Return the durable per-sample archive maintained by ``perf_backup.py``.
|
||||||
|
|
||||||
|
The archive contains every sample ever produced across all runs and
|
||||||
|
breakpoints for this benchmark/model (deduplicated by ``index``). When
|
||||||
|
present, the summary aggregator should prefer it over the raw
|
||||||
|
``predictions/*.jsonl`` files because the latter can be overwritten on
|
||||||
|
restart.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from perf_backup import get_backup_paths
|
||||||
|
except ImportError:
|
||||||
|
return []
|
||||||
|
_, archive_path = get_backup_paths(output_dir, benchmark, model_name)
|
||||||
|
if archive_path.exists():
|
||||||
|
return [archive_path]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def find_all_predictions(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Find all predictions JSONL files for a benchmark/model.
|
||||||
|
|
||||||
|
The durable ``predictions_archive/<benchmark>__<model>.jsonl`` (if
|
||||||
|
present) is returned **first** so its deduplicated per-sample records
|
||||||
|
dominate any smaller predictions that a fresh run may have written.
|
||||||
|
"""
|
||||||
|
files = list(find_archive_predictions(output_dir, benchmark, model_name))
|
||||||
|
bench_dir = output_dir / benchmark
|
||||||
|
if not bench_dir.exists():
|
||||||
|
return files
|
||||||
|
for seed_dir in sorted(bench_dir.iterdir()):
|
||||||
|
if not seed_dir.is_dir():
|
||||||
|
continue
|
||||||
|
pred_dir = seed_dir / 'predictions' / model_name
|
||||||
|
if pred_dir.exists():
|
||||||
|
files.extend(sorted(pred_dir.rglob('*.jsonl')))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log_duration(log_file: Path):
|
||||||
|
"""Parse first and last timestamp from eval_log.log and return duration in hours."""
|
||||||
|
if not log_file.exists():
|
||||||
|
return np.nan
|
||||||
|
from datetime import datetime
|
||||||
|
first_dt = None
|
||||||
|
last_dt = None
|
||||||
|
fmt = '%Y-%m-%d %H:%M:%S'
|
||||||
|
with open(log_file, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if len(line) < 19:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(line[:19], fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if first_dt is None:
|
||||||
|
first_dt = dt
|
||||||
|
last_dt = dt
|
||||||
|
if first_dt is None or last_dt is None or last_dt <= first_dt:
|
||||||
|
return np.nan
|
||||||
|
return (last_dt - first_dt).total_seconds() / 3600.0
|
||||||
|
|
||||||
|
|
||||||
|
def extract_score(report_data: dict) -> float:
|
||||||
|
"""Extract the top-level score from a report JSON."""
|
||||||
|
score = report_data.get('score')
|
||||||
|
if score is not None:
|
||||||
|
return float(score)
|
||||||
|
metrics = report_data.get('metrics', [])
|
||||||
|
for m in metrics:
|
||||||
|
if m.get('name') == 'mean_acc':
|
||||||
|
return float(m.get('score', m.get('macro_score', 0.0)))
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def collect_benchmark(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Collect aggregated results for one benchmark."""
|
||||||
|
reports = find_all_reports(output_dir, benchmark, model_name)
|
||||||
|
if not reports:
|
||||||
|
return None
|
||||||
|
|
||||||
|
scores = []
|
||||||
|
summary0 = None
|
||||||
|
n_samples_unique = 0
|
||||||
|
for report in reports:
|
||||||
|
try:
|
||||||
|
data = json.loads(report.read_text(encoding='utf-8'))
|
||||||
|
scores.append(extract_score(data))
|
||||||
|
if summary0 is None:
|
||||||
|
summary0 = data.get('perf_metrics', {}).get('summary', {})
|
||||||
|
n_samples_unique = summary0.get('n_samples', 0)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
avg_score = float(np.mean(scores)) if scores else 0.0
|
||||||
|
|
||||||
|
# Aggregate raw prediction perf metrics across all seeds/runs.
|
||||||
|
# This fixes the breakpoint-resume issue where cumulative stats are reset.
|
||||||
|
# Predictions are deduplicated by sample `index` so the archive (which
|
||||||
|
# spans every run) and the latest ``predictions/*.jsonl`` don't double
|
||||||
|
# count the same sample.
|
||||||
|
pred_files = find_all_predictions(output_dir, benchmark, model_name)
|
||||||
|
seen_indexes = set()
|
||||||
|
latencies = []
|
||||||
|
ttfts = []
|
||||||
|
tpots = []
|
||||||
|
input_tokens = []
|
||||||
|
output_tokens = []
|
||||||
|
sample_indexes = []
|
||||||
|
for pf in pred_files:
|
||||||
|
for obj in read_predictions_with_index(pf):
|
||||||
|
idx = obj['index']
|
||||||
|
if idx is None or idx in seen_indexes:
|
||||||
|
# Still keep perf data even when we lack an index, so older
|
||||||
|
# benchmark files without `index` don't get dropped.
|
||||||
|
pm = obj['perf_metrics']
|
||||||
|
else:
|
||||||
|
seen_indexes.add(idx)
|
||||||
|
sample_indexes.append(idx)
|
||||||
|
pm = obj['perf_metrics']
|
||||||
|
if pm is None:
|
||||||
|
continue
|
||||||
|
if 'latency' in pm:
|
||||||
|
latencies.append(float(pm['latency']))
|
||||||
|
if 'ttft' in pm:
|
||||||
|
ttfts.append(float(pm['ttft']))
|
||||||
|
if 'tpot' in pm:
|
||||||
|
tpots.append(float(pm['tpot']))
|
||||||
|
|
||||||
|
itok = pm.get('input_tokens')
|
||||||
|
otok = pm.get('output_tokens')
|
||||||
|
if itok is None and 'usage' in pm:
|
||||||
|
itok = pm['usage'].get('input_tokens')
|
||||||
|
otok = pm['usage'].get('output_tokens')
|
||||||
|
if itok is not None:
|
||||||
|
input_tokens.append(int(itok))
|
||||||
|
if otok is not None:
|
||||||
|
output_tokens.append(int(otok))
|
||||||
|
|
||||||
|
# If we don't have raw predictions but have a perf_stats backup, that
|
||||||
|
# represents a known-good summary captured right after a clean run —
|
||||||
|
# preferable to summary0 (which may be the just-reset run).
|
||||||
|
backup_summary = None
|
||||||
|
if not latencies:
|
||||||
|
backup_summary = load_backup_summary(output_dir, benchmark, model_name)
|
||||||
|
|
||||||
|
if latencies:
|
||||||
|
latency_mean = float(np.mean(latencies))
|
||||||
|
total_compute_time = float(np.sum(latencies))
|
||||||
|
total_output_tokens = sum(output_tokens)
|
||||||
|
avg_output_tps = total_output_tokens / total_compute_time if total_compute_time > 0 else np.nan
|
||||||
|
avg_req_ps = len(latencies) / total_compute_time if total_compute_time > 0 else np.nan
|
||||||
|
input_tok_mean = float(np.mean(input_tokens)) if input_tokens else np.nan
|
||||||
|
output_tok_mean = float(np.mean(output_tokens)) if output_tokens else np.nan
|
||||||
|
total_tokens = sum(input_tokens) + sum(output_tokens)
|
||||||
|
ttft_mean = float(np.mean(ttfts)) if ttfts else np.nan
|
||||||
|
ttft_p90 = percentile(ttfts, 90)
|
||||||
|
ttft_p99 = percentile(ttfts, 99)
|
||||||
|
tpot_mean = float(np.mean(tpots)) if tpots else np.nan
|
||||||
|
tpot_p90 = percentile(tpots, 90)
|
||||||
|
tpot_p99 = percentile(tpots, 99)
|
||||||
|
# The actual sample count we just rebuilt from raw predictions is more
|
||||||
|
# reliable than the (possibly reset) report summary's n_samples.
|
||||||
|
if sample_indexes:
|
||||||
|
n_samples_unique = len(sample_indexes)
|
||||||
|
elif summary0:
|
||||||
|
# Fallback to report summary if raw predictions are unavailable
|
||||||
|
latency_mean = summary0.get('latency', {}).get('mean', np.nan)
|
||||||
|
avg_output_tps = summary0.get('throughput', {}).get('avg_output_tps', np.nan)
|
||||||
|
avg_req_ps = summary0.get('throughput', {}).get('avg_req_ps', np.nan)
|
||||||
|
input_tok_mean = summary0.get('tokens', {}).get('input_tokens', {}).get('mean', np.nan)
|
||||||
|
output_tok_mean = summary0.get('tokens', {}).get('output_tokens', {}).get('mean', np.nan)
|
||||||
|
total_tokens = summary0.get('tokens', {}).get('total_tokens_count', np.nan)
|
||||||
|
ttft_mean = summary0.get('ttft', {}).get('mean', np.nan)
|
||||||
|
ttft_p90 = summary0.get('ttft', {}).get('90%', np.nan)
|
||||||
|
ttft_p99 = summary0.get('ttft', {}).get('99%', np.nan)
|
||||||
|
tpot_mean = summary0.get('tpot', {}).get('mean', np.nan)
|
||||||
|
tpot_p90 = summary0.get('tpot', {}).get('90%', np.nan)
|
||||||
|
tpot_p99 = summary0.get('tpot', {}).get('99%', np.nan)
|
||||||
|
# If the just-read report looks like a freshly-reset run (smaller
|
||||||
|
# n_samples than the durable backup), prefer the backup's summary so
|
||||||
|
# the cumulative numbers are not lost.
|
||||||
|
try:
|
||||||
|
from perf_backup import get_backup_paths
|
||||||
|
backup_path, _ = get_backup_paths(output_dir, benchmark, model_name)
|
||||||
|
if backup_path.exists():
|
||||||
|
payload = json.loads(backup_path.read_text(encoding='utf-8'))
|
||||||
|
if (payload.get('n_samples') or 0) > (summary0.get('n_samples') or 0):
|
||||||
|
backup_summary = payload.get('summary') or {}
|
||||||
|
latency_mean = backup_summary.get('latency', {}).get('mean', latency_mean)
|
||||||
|
avg_output_tps = backup_summary.get('throughput', {}).get('avg_output_tps', avg_output_tps)
|
||||||
|
avg_req_ps = backup_summary.get('throughput', {}).get('avg_req_ps', avg_req_ps)
|
||||||
|
input_tok_mean = backup_summary.get('usage', {}).get('input_tokens', {}).get('mean', input_tok_mean)
|
||||||
|
output_tok_mean = backup_summary.get('usage', {}).get('output_tokens', {}).get('mean', output_tok_mean)
|
||||||
|
total_tokens = backup_summary.get('usage', {}).get('total_tokens_count', total_tokens)
|
||||||
|
ttft_mean = backup_summary.get('ttft', {}).get('mean', ttft_mean)
|
||||||
|
ttft_p90 = backup_summary.get('ttft', {}).get('90%', ttft_p90)
|
||||||
|
ttft_p99 = backup_summary.get('ttft', {}).get('99%', ttft_p99)
|
||||||
|
tpot_mean = backup_summary.get('tpot', {}).get('mean', tpot_mean)
|
||||||
|
tpot_p90 = backup_summary.get('tpot', {}).get('90%', tpot_p90)
|
||||||
|
tpot_p99 = backup_summary.get('tpot', {}).get('99%', tpot_p99)
|
||||||
|
n_samples_unique = payload.get('n_samples') or n_samples_unique
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif backup_summary:
|
||||||
|
latency_mean = backup_summary.get('latency', {}).get('mean', np.nan)
|
||||||
|
avg_output_tps = backup_summary.get('throughput', {}).get('avg_output_tps', np.nan)
|
||||||
|
avg_req_ps = backup_summary.get('throughput', {}).get('avg_req_ps', np.nan)
|
||||||
|
input_tok_mean = backup_summary.get('usage', {}).get('input_tokens', {}).get('mean', np.nan)
|
||||||
|
output_tok_mean = backup_summary.get('usage', {}).get('output_tokens', {}).get('mean', np.nan)
|
||||||
|
total_tokens = backup_summary.get('usage', {}).get('total_tokens_count', np.nan)
|
||||||
|
ttft_mean = backup_summary.get('ttft', {}).get('mean', np.nan)
|
||||||
|
ttft_p90 = backup_summary.get('ttft', {}).get('90%', np.nan)
|
||||||
|
ttft_p99 = backup_summary.get('ttft', {}).get('99%', np.nan)
|
||||||
|
tpot_mean = backup_summary.get('tpot', {}).get('mean', np.nan)
|
||||||
|
tpot_p90 = backup_summary.get('tpot', {}).get('90%', np.nan)
|
||||||
|
tpot_p99 = backup_summary.get('tpot', {}).get('99%', np.nan)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Duration: prefer the durable active timer maintained by perf_backup.py,
|
||||||
|
# which only counts time when run_task() is actually executing. This avoids
|
||||||
|
# counting idle gaps caused by manual interruption/resume.
|
||||||
|
# If no active timer exists, fall back to log duration or latency estimate.
|
||||||
|
active_time_hours = np.nan
|
||||||
|
try:
|
||||||
|
from perf_backup import load_active_time
|
||||||
|
active_seconds = load_active_time(output_dir, benchmark, model_name)
|
||||||
|
if active_seconds > 0:
|
||||||
|
active_time_hours = active_seconds / 3600.0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not np.isnan(active_time_hours):
|
||||||
|
duration_hours = active_time_hours
|
||||||
|
else:
|
||||||
|
# Legacy fallback: sum of wall-clock durations from eval_log.log.
|
||||||
|
# For sandbox benchmarks the main log may not cover sandbox execution,
|
||||||
|
# so we also estimate wall time as compute_time/batch_size and take max.
|
||||||
|
duration_hours = 0.0
|
||||||
|
batch_size = 4
|
||||||
|
for report in reports:
|
||||||
|
log_file = report.parent.parent.parent / 'logs' / 'eval_log.log'
|
||||||
|
d = parse_log_duration(log_file)
|
||||||
|
if not np.isnan(d):
|
||||||
|
duration_hours += d
|
||||||
|
cfg_file = report.parent.parent.parent / 'configs' / 'task_config.yaml'
|
||||||
|
if cfg_file.exists():
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
cfg = yaml.safe_load(cfg_file.read_text(encoding='utf-8'))
|
||||||
|
batch_size = int(cfg.get('eval_batch_size', batch_size))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
compute_wall_estimate = (total_compute_time / batch_size / 3600.0) if latencies and batch_size > 0 else np.nan
|
||||||
|
if duration_hours <= 0:
|
||||||
|
duration_hours = compute_wall_estimate
|
||||||
|
else:
|
||||||
|
duration_hours = max(duration_hours, compute_wall_estimate)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'分类': BENCHMARK_DOMAIN.get(benchmark, '其他'),
|
||||||
|
'Benchmark': BENCHMARK_NAME_ALIAS.get(benchmark, benchmark),
|
||||||
|
'得分': round(avg_score, 4),
|
||||||
|
'实测时间(h)': round(duration_hours, 4) if not np.isnan(duration_hours) else np.nan,
|
||||||
|
'总样本数': n_samples_unique,
|
||||||
|
'延迟_mean(s)': round(latency_mean, 5) if not np.isnan(latency_mean) else np.nan,
|
||||||
|
'输出TPS': round(avg_output_tps, 2) if not np.isnan(avg_output_tps) else np.nan,
|
||||||
|
'请求QPS': round(avg_req_ps, 4) if not np.isnan(avg_req_ps) else np.nan,
|
||||||
|
'输入tokens_mean': round(input_tok_mean, 2) if not np.isnan(input_tok_mean) else np.nan,
|
||||||
|
'输出tokens_mean': round(output_tok_mean, 2) if not np.isnan(output_tok_mean) else np.nan,
|
||||||
|
'累计总tokens': total_tokens if not np.isnan(total_tokens) else np.nan,
|
||||||
|
'TTFT_mean(s)': round(ttft_mean, 5) if not np.isnan(ttft_mean) else np.nan,
|
||||||
|
'TTFT P90': round(ttft_p90, 5) if not np.isnan(ttft_p90) else np.nan,
|
||||||
|
'TTFT P99': round(ttft_p99, 5) if not np.isnan(ttft_p99) else np.nan,
|
||||||
|
'TPOT_mean(s)': round(tpot_mean, 5) if not np.isnan(tpot_mean) else np.nan,
|
||||||
|
'TPOT P90': round(tpot_p90, 5) if not np.isnan(tpot_p90) else np.nan,
|
||||||
|
'TPOT P99': round(tpot_p99, 5) if not np.isnan(tpot_p99) else np.nan,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def eval_benchmark(benchmark_names: list, output_dir: Path, model_name: str,
|
||||||
|
out_name: str = None, excel_output_dir: Path = None):
|
||||||
|
"""Collect results for a specific list of benchmarks.
|
||||||
|
|
||||||
|
This is the public entry point intended for use by ``run.py`` and other
|
||||||
|
scripts that already know which benchmarks they ran.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
benchmark_names: List of canonical benchmark names to aggregate (e.g.
|
||||||
|
``['aime24', 'gsm8k', 'arc']``). Only benchmarks with reports on
|
||||||
|
disk will appear in the summary.
|
||||||
|
output_dir: EvalScope output root directory.
|
||||||
|
model_name: Model name to look up reports/predictions for.
|
||||||
|
out_name: Output file name (without extension). Defaults to the safe
|
||||||
|
model name.
|
||||||
|
excel_output_dir: Optional separate directory for the Excel summary.
|
||||||
|
If provided, the Excel file is written here while the CSV stays
|
||||||
|
under ``output_dir/results/<model>/``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(csv_path, xlsx_path) tuple.
|
||||||
|
"""
|
||||||
|
return collect_all(output_dir, model_name, out_name,
|
||||||
|
include_benchmarks=benchmark_names,
|
||||||
|
excel_output_dir=excel_output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_all(output_dir: Path, model_name: str, out_name: str = None,
|
||||||
|
include_benchmarks: list = None,
|
||||||
|
excel_output_dir: Path = None):
|
||||||
|
"""Collect benchmarks under ``output_dir`` and write summary Excel/CSV."""
|
||||||
|
if not output_dir.exists():
|
||||||
|
raise FileNotFoundError(f'Output directory not found: {output_dir}')
|
||||||
|
|
||||||
|
safe_model = model_name.replace('/', '_').replace('\\', '_').replace(' ', '_')
|
||||||
|
|
||||||
|
# Discover benchmarks
|
||||||
|
if include_benchmarks is not None:
|
||||||
|
# Whitelist mode: only aggregate benchmarks that were actually run in
|
||||||
|
# this invocation. Directory names may differ from canonical names
|
||||||
|
# (e.g. evalscope writes the hle benchmark to the hle_low directory),
|
||||||
|
# so we canonicalize each directory name via BENCHMARK_NAME_ALIAS and
|
||||||
|
# check membership in the caller's list. Order follows the caller's
|
||||||
|
# include_benchmarks list.
|
||||||
|
include_set = set(include_benchmarks)
|
||||||
|
discovered = [] # list of (dir_name, canonical_name)
|
||||||
|
for bench_dir in output_dir.iterdir():
|
||||||
|
if not bench_dir.is_dir() or bench_dir.name == safe_model:
|
||||||
|
continue
|
||||||
|
if not find_all_reports(output_dir, bench_dir.name, model_name):
|
||||||
|
continue
|
||||||
|
canonical = BENCHMARK_NAME_ALIAS.get(bench_dir.name, bench_dir.name)
|
||||||
|
if canonical in include_set or bench_dir.name in include_set:
|
||||||
|
discovered.append((bench_dir.name, canonical))
|
||||||
|
|
||||||
|
order = {name: idx for idx, name in enumerate(include_benchmarks)}
|
||||||
|
discovered.sort(key=lambda x: order.get(x[1], len(include_benchmarks)))
|
||||||
|
|
||||||
|
benchmarks = [dir_name for dir_name, _ in discovered]
|
||||||
|
else:
|
||||||
|
# Legacy behaviour: scan everything on disk.
|
||||||
|
benchmarks = []
|
||||||
|
for bench_dir in sorted(output_dir.iterdir()):
|
||||||
|
if not bench_dir.is_dir() or bench_dir.name == safe_model:
|
||||||
|
continue
|
||||||
|
reports = find_all_reports(output_dir, bench_dir.name, model_name)
|
||||||
|
if reports:
|
||||||
|
benchmarks.append(bench_dir.name)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for benchmark in benchmarks:
|
||||||
|
row = collect_benchmark(output_dir, benchmark, model_name)
|
||||||
|
if row:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print(f'No results found for model {model_name} under {output_dir}')
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
df = pd.DataFrame(rows, columns=OUTPUT_COLUMNS)
|
||||||
|
df = df.drop_duplicates(subset=['Benchmark'], keep='first')
|
||||||
|
|
||||||
|
# Add total row
|
||||||
|
total_score = df['得分'].mean()
|
||||||
|
total_time = df['实测时间(h)'].sum()
|
||||||
|
total_row = {
|
||||||
|
'分类': '总计',
|
||||||
|
'Benchmark': '',
|
||||||
|
'得分': round(total_score, 4),
|
||||||
|
'实测时间(h)': round(total_time, 4),
|
||||||
|
}
|
||||||
|
for col in OUTPUT_COLUMNS:
|
||||||
|
if col not in total_row:
|
||||||
|
total_row[col] = np.nan
|
||||||
|
df = pd.concat([df, pd.DataFrame([total_row], columns=OUTPUT_COLUMNS)], ignore_index=True)
|
||||||
|
|
||||||
|
summary_dir = output_dir / 'results' / safe_model
|
||||||
|
summary_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if out_name is None:
|
||||||
|
out_name = safe_model
|
||||||
|
|
||||||
|
csv_path = summary_dir / f'{out_name}.csv'
|
||||||
|
|
||||||
|
# Excel can optionally be written to a separate project-level results dir
|
||||||
|
# so that the latest summary per model is easy to find independently of
|
||||||
|
# the per-run output directory.
|
||||||
|
if excel_output_dir is not None:
|
||||||
|
xlsx_dir = Path(excel_output_dir)
|
||||||
|
xlsx_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
xlsx_path = xlsx_dir / f'{out_name}.xlsx'
|
||||||
|
else:
|
||||||
|
xlsx_path = summary_dir / f'{out_name}.xlsx'
|
||||||
|
|
||||||
|
df.to_csv(csv_path, index=False, encoding='utf-8-sig')
|
||||||
|
df.to_excel(xlsx_path, index=False)
|
||||||
|
|
||||||
|
print(f'Summary written to:')
|
||||||
|
print(f' CSV: {csv_path}')
|
||||||
|
print(f' Excel: {xlsx_path}')
|
||||||
|
return csv_path, xlsx_path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Collect EvalScope benchmark results into Excel/CSV')
|
||||||
|
parser.add_argument('--output-dir', required=True, help='EvalScope output directory')
|
||||||
|
parser.add_argument('--model', default='DeepSeek-V4-Flash-Int8', help='Model name used in reports')
|
||||||
|
parser.add_argument('--out-name', default=None, help='Output file name (without extension); defaults to safe model name')
|
||||||
|
parser.add_argument('--include', default=None,
|
||||||
|
help='Comma-separated benchmark whitelist (default: all on disk)')
|
||||||
|
parser.add_argument('--benchmarks', '--benchmarks', dest='benchmarks', default=None,
|
||||||
|
help='Alias for --include')
|
||||||
|
parser.add_argument('--excel-output-dir', default=None,
|
||||||
|
help='Separate directory for the Excel summary; defaults to <project_root>/results')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Default Excel output dir to project-level results/ so the latest summary
|
||||||
|
# per model is easy to find independently of the per-run output directory.
|
||||||
|
if args.excel_output_dir is None:
|
||||||
|
excel_output_dir = Path(__file__).parent.parent / 'results'
|
||||||
|
else:
|
||||||
|
excel_output_dir = Path(args.excel_output_dir)
|
||||||
|
|
||||||
|
include_benchmarks = None
|
||||||
|
if args.include:
|
||||||
|
include_benchmarks = [b.strip() for b in args.include.split(',') if b.strip()]
|
||||||
|
elif args.benchmarks:
|
||||||
|
include_benchmarks = [b.strip() for b in args.benchmarks.split(',') if b.strip()]
|
||||||
|
|
||||||
|
if include_benchmarks is not None:
|
||||||
|
eval_benchmark(include_benchmarks, Path(args.output_dir), args.model, args.out_name,
|
||||||
|
excel_output_dir=excel_output_dir)
|
||||||
|
else:
|
||||||
|
collect_all(Path(args.output_dir), args.model, args.out_name,
|
||||||
|
excel_output_dir=excel_output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
241
bash/perf_backup.py
Normal file
241
bash/perf_backup.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Perf-stats backup helpers.
|
||||||
|
|
||||||
|
EvalScope writes the per-benchmark cumulative perf summary into
|
||||||
|
``report.json`` under ``perf_metrics.summary`` and the raw per-sample records
|
||||||
|
into ``predictions/*.jsonl``. Both are overwritten on every run, so a
|
||||||
|
checkpoint restart resets cumulative stats and loses prior samples.
|
||||||
|
|
||||||
|
This module maintains **two durable files** under ``<output_dir>/`` so the
|
||||||
|
breakpoint-resume issue can be reconstructed:
|
||||||
|
|
||||||
|
1. ``perf_stats_backup/<benchmark>__<model>.json``
|
||||||
|
- The latest complete ``perf_metrics.summary`` captured right after
|
||||||
|
each ``run_task()`` finishes.
|
||||||
|
2. ``predictions_archive/<benchmark>__<model>.jsonl``
|
||||||
|
- All per-sample records ever seen, deduplicated by sample ``index``.
|
||||||
|
|
||||||
|
``bash.collect_results`` prefers these archive files when aggregating metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PERF_STATS_BACKUP_DIRNAME = 'perf_stats_backup'
|
||||||
|
PREDICTIONS_ARCHIVE_DIRNAME = 'predictions_archive'
|
||||||
|
ACTIVE_TIME_DIRNAME = 'active_time'
|
||||||
|
|
||||||
|
|
||||||
|
def _safe(s: str) -> str:
|
||||||
|
return s.replace('/', '_').replace('\\', '_').replace(' ', '_')
|
||||||
|
|
||||||
|
|
||||||
|
def get_backup_paths(output_dir: Path, benchmark: str, model_name: str):
|
||||||
|
"""Return the (perf_stats_backup, predictions_archive) paths for a benchmark/model."""
|
||||||
|
safe = _safe(model_name)
|
||||||
|
backup_dir = Path(output_dir) / PERF_STATS_BACKUP_DIRNAME
|
||||||
|
archive_dir = Path(output_dir) / PREDICTIONS_ARCHIVE_DIRNAME
|
||||||
|
perf_stats_path = backup_dir / f'{_safe(benchmark)}__{safe}.json'
|
||||||
|
predictions_path = archive_dir / f'{_safe(benchmark)}__{safe}.jsonl'
|
||||||
|
return perf_stats_path, predictions_path
|
||||||
|
|
||||||
|
|
||||||
|
def backup_perf_stats(output_dir: Path, benchmark: str, model_name: str,
|
||||||
|
report_json: Path) -> Path:
|
||||||
|
"""Copy ``perf_metrics.summary`` from a finished report to the durable backup.
|
||||||
|
|
||||||
|
Only writes if ``report_json`` exists and contains a ``perf_metrics.summary``.
|
||||||
|
Returns the backup path.
|
||||||
|
"""
|
||||||
|
if not report_json or not report_json.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(report_json.read_text(encoding='utf-8'))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
summary = (data.get('perf_metrics') or {}).get('summary')
|
||||||
|
if not summary:
|
||||||
|
return None
|
||||||
|
|
||||||
|
perf_stats_path, _ = get_backup_paths(Path(output_dir), benchmark, model_name)
|
||||||
|
perf_stats_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
'benchmark': benchmark,
|
||||||
|
'model': model_name,
|
||||||
|
'updated_at': datetime.now().isoformat(timespec='seconds'),
|
||||||
|
'n_samples': summary.get('n_samples'),
|
||||||
|
'summary': summary,
|
||||||
|
}
|
||||||
|
# Don't overwrite a strictly larger backup with a smaller one — that
|
||||||
|
# would be the symptom of a reset run and we want to keep the larger.
|
||||||
|
if perf_stats_path.exists():
|
||||||
|
try:
|
||||||
|
old = json.loads(perf_stats_path.read_text(encoding='utf-8'))
|
||||||
|
old_n = old.get('n_samples') or 0
|
||||||
|
new_n = summary.get('n_samples') or 0
|
||||||
|
if new_n < old_n:
|
||||||
|
# Don't clobber the larger historical backup.
|
||||||
|
return perf_stats_path
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
perf_stats_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||||
|
return perf_stats_path
|
||||||
|
|
||||||
|
|
||||||
|
def archive_predictions(output_dir: Path, benchmark: str, model_name: str,
|
||||||
|
predictions_dir: Path) -> Path:
|
||||||
|
"""Append the latest predictions into the durable archive.
|
||||||
|
|
||||||
|
Reads every ``*.jsonl`` under ``predictions_dir``, parses each line, and
|
||||||
|
appends each sample to the archive file (deduplicated by ``index``). If
|
||||||
|
the same ``index`` was already archived with a different perf payload,
|
||||||
|
the newer record wins.
|
||||||
|
|
||||||
|
Returns the archive path.
|
||||||
|
"""
|
||||||
|
_, archive_path = get_backup_paths(Path(output_dir), benchmark, model_name)
|
||||||
|
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not predictions_dir or not predictions_dir.exists():
|
||||||
|
return archive_path
|
||||||
|
|
||||||
|
# Load existing archive (index -> record)
|
||||||
|
existing = {}
|
||||||
|
if archive_path.exists():
|
||||||
|
with archive_path.open('r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
idx = obj.get('index')
|
||||||
|
if idx is not None:
|
||||||
|
existing[idx] = obj
|
||||||
|
|
||||||
|
# Add new predictions
|
||||||
|
new_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
for jsonl in sorted(predictions_dir.rglob('*.jsonl')):
|
||||||
|
with jsonl.open('r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
obj = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
idx = obj.get('index')
|
||||||
|
if idx is None:
|
||||||
|
continue
|
||||||
|
if idx in existing:
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
new_count += 1
|
||||||
|
existing[idx] = obj
|
||||||
|
|
||||||
|
# Write back atomically
|
||||||
|
tmp_path = archive_path.with_suffix('.jsonl.tmp')
|
||||||
|
with tmp_path.open('w', encoding='utf-8') as f:
|
||||||
|
for idx in sorted(existing.keys()):
|
||||||
|
f.write(json.dumps(existing[idx], ensure_ascii=False))
|
||||||
|
f.write('\n')
|
||||||
|
shutil.move(str(tmp_path), str(archive_path))
|
||||||
|
|
||||||
|
return archive_path
|
||||||
|
|
||||||
|
|
||||||
|
def restore_from_backup(output_dir: Path, benchmark: str, model_name: str,
|
||||||
|
target_predictions_dir: Path, target_report_json: Path):
|
||||||
|
"""If both ``perf_stats_backup`` and ``predictions_archive`` exist for a
|
||||||
|
benchmark/model, materialise them into ``target_predictions_dir`` and
|
||||||
|
``target_report_json`` before the next run starts, so evalscope resumes
|
||||||
|
from the larger historical state instead of overwriting it.
|
||||||
|
|
||||||
|
Returns ``True`` if a backup was restored, ``False`` otherwise.
|
||||||
|
"""
|
||||||
|
perf_stats_path, archive_path = get_backup_paths(Path(output_dir), benchmark, model_name)
|
||||||
|
if not perf_stats_path.exists() and not archive_path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Restore the archive into the predictions directory so evalscope's
|
||||||
|
# use_cache mechanism sees the previously-completed samples.
|
||||||
|
if archive_path.exists():
|
||||||
|
target_predictions_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_jsonl = target_predictions_dir / archive_path.name
|
||||||
|
shutil.copy2(archive_path, target_jsonl)
|
||||||
|
|
||||||
|
# Restore the perf summary into the report file so the cumulative stats
|
||||||
|
# survive. We do NOT overwrite score/metrics — only the perf section.
|
||||||
|
if perf_stats_path.exists() and target_report_json.exists():
|
||||||
|
try:
|
||||||
|
payload = json.loads(perf_stats_path.read_text(encoding='utf-8'))
|
||||||
|
summary = payload.get('summary')
|
||||||
|
if not summary:
|
||||||
|
return True
|
||||||
|
report = json.loads(target_report_json.read_text(encoding='utf-8'))
|
||||||
|
report.setdefault('perf_metrics', {})['summary'] = summary
|
||||||
|
target_report_json.write_text(
|
||||||
|
json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8'
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Active run-time tracking (excludes idle gaps between resumes)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_time_path(output_dir: Path, benchmark: str, model_name: str) -> Path:
|
||||||
|
"""Return the path to the durable active-time record for a benchmark/model."""
|
||||||
|
safe = _safe(model_name)
|
||||||
|
active_dir = Path(output_dir) / ACTIVE_TIME_DIRNAME
|
||||||
|
return active_dir / f'{_safe(benchmark)}__{safe}.json'
|
||||||
|
|
||||||
|
|
||||||
|
def record_active_time(output_dir: Path, benchmark: str, model_name: str,
|
||||||
|
seconds: float) -> Path:
|
||||||
|
"""Accumulate ``seconds`` of active run time for a benchmark/model.
|
||||||
|
|
||||||
|
This is intended to measure only the time ``run_task()`` was actually
|
||||||
|
executing, excluding idle gaps caused by manual interruption/resume.
|
||||||
|
"""
|
||||||
|
path = get_active_time_path(Path(output_dir), benchmark, model_name)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
data = {'total_seconds': 0.0}
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data['total_seconds'] = float(data.get('total_seconds', 0.0)) + float(seconds)
|
||||||
|
data['updated_at'] = datetime.now().isoformat(timespec='seconds')
|
||||||
|
data['benchmark'] = benchmark
|
||||||
|
data['model'] = model_name
|
||||||
|
|
||||||
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def load_active_time(output_dir: Path, benchmark: str, model_name: str) -> float:
|
||||||
|
"""Return accumulated active run time in seconds, or 0.0 if no record."""
|
||||||
|
path = get_active_time_path(Path(output_dir), benchmark, model_name)
|
||||||
|
if not path.exists():
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding='utf-8'))
|
||||||
|
return float(data.get('total_seconds', 0.0))
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
329
bash/pull_swe_bench_images.py
Executable file
329
bash/pull_swe_bench_images.py
Executable file
@ -0,0 +1,329 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
预拉取 SWE-bench 评测所需的 Docker 镜像,支持断点续传。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
# 默认从上次进度继续
|
||||||
|
python bash/pull_swe_bench_images.py --dataset swe_bench_verified --max-workers 4
|
||||||
|
|
||||||
|
# 强制重新开始(删除进度文件)
|
||||||
|
rm -f output/.swe_bench_pull_state.json
|
||||||
|
python bash/pull_swe_bench_images.py --dataset swe_bench_verified --max-workers 4
|
||||||
|
|
||||||
|
# 测试前 3 个镜像
|
||||||
|
python bash/pull_swe_bench_images.py --dry-run 3 --max-workers 1
|
||||||
|
|
||||||
|
进度文件:
|
||||||
|
output/.swe_bench_pull_state.json
|
||||||
|
记录已拉取/跳过/失败的镜像,Ctrl+C 后下次自动跳过。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 国内网络加速(必须在导入 evalscope/huggingface 之前设置)
|
||||||
|
os.environ.setdefault('USE_MODELSCOPE_HUB', '1')
|
||||||
|
os.environ.setdefault('HF_ENDPOINT', 'https://hf-mirror.com')
|
||||||
|
os.environ.setdefault('PYTHONUNBUFFERED', '1')
|
||||||
|
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
# 让 evalscope 可导入
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT / 'evalscope'))
|
||||||
|
|
||||||
|
from evalscope.api.dataset import RemoteDataLoader, FieldSpec
|
||||||
|
|
||||||
|
|
||||||
|
DATASET_IDS = {
|
||||||
|
'swe_bench_verified': 'princeton-nlp/SWE-bench_Verified',
|
||||||
|
# 'swe_bench_lite': 'princeton-nlp/SWE-bench_Lite',
|
||||||
|
# 'swe_bench_verified_mini': 'evalscope/swe-bench-verified-mini',
|
||||||
|
# 'swe_bench_pro': 'evalscope/swe-bench-pro',
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / 'output'
|
||||||
|
STATE_FILENAME = '.swe_bench_pull_state.json'
|
||||||
|
|
||||||
|
|
||||||
|
def get_state_path(output_dir: Path) -> Path:
|
||||||
|
return Path(output_dir) / STATE_FILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(output_dir: Path) -> dict:
|
||||||
|
"""Load previous pull state; return empty state if none exists."""
|
||||||
|
state_path = get_state_path(output_dir)
|
||||||
|
if not state_path.exists():
|
||||||
|
return {'done': [], 'skipped': [], 'failed': []}
|
||||||
|
try:
|
||||||
|
with state_path.open('r', encoding='utf-8') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
# Ensure required keys exist
|
||||||
|
for key in ('done', 'skipped', 'failed'):
|
||||||
|
state.setdefault(key, [])
|
||||||
|
return state
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: failed to load state file {state_path}: {e}')
|
||||||
|
return {'done': [], 'skipped': [], 'failed': []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(output_dir: Path, state: dict):
|
||||||
|
"""Atomically save current pull state."""
|
||||||
|
state_path = get_state_path(output_dir)
|
||||||
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp_path = state_path.with_suffix('.tmp')
|
||||||
|
try:
|
||||||
|
with tmp_path.open('w', encoding='utf-8') as f:
|
||||||
|
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||||
|
tmp_path.replace(state_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: failed to save state file {state_path}: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def load_swe_samples(dataset_id: str):
|
||||||
|
"""Load SWE-bench samples and wrap them into the format expected by build_images."""
|
||||||
|
print(f'正在加载数据集元数据: {dataset_id} ...')
|
||||||
|
loader = RemoteDataLoader(
|
||||||
|
data_id_or_path=dataset_id,
|
||||||
|
split='test',
|
||||||
|
sample_fields=FieldSpec(
|
||||||
|
input='problem_statement',
|
||||||
|
metadata=[
|
||||||
|
'instance_id',
|
||||||
|
'repo',
|
||||||
|
'base_commit',
|
||||||
|
'patch',
|
||||||
|
'PASS_TO_PASS',
|
||||||
|
'FAIL_TO_PASS',
|
||||||
|
'test_patch',
|
||||||
|
'version',
|
||||||
|
'environment_setup_commit',
|
||||||
|
'hints_text',
|
||||||
|
'created_at',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return loader.load()
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_name(instance, namespace: str, arch: str) -> str:
|
||||||
|
"""根据 swebench 规则生成远程镜像名称。"""
|
||||||
|
if isinstance(instance, dict):
|
||||||
|
metadata = instance
|
||||||
|
else:
|
||||||
|
metadata = getattr(instance, 'metadata', {}) or {}
|
||||||
|
|
||||||
|
instance_id = metadata.get('instance_id', '')
|
||||||
|
if not instance_id and not isinstance(instance, dict):
|
||||||
|
instance_id = getattr(instance, 'instance_id', '')
|
||||||
|
|
||||||
|
if not instance_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
image_name = f'{namespace}/sweb.eval.{arch}.{instance_id.lower()}:latest'
|
||||||
|
image_name = image_name.replace('__', '_1776_')
|
||||||
|
return image_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_images() -> set:
|
||||||
|
"""Return a set of all locally available Docker image names."""
|
||||||
|
result = subprocess.run(
|
||||||
|
['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return set()
|
||||||
|
return {line.strip() for line in result.stdout.splitlines() if line.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
_print_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _log(msg: str):
|
||||||
|
with _print_lock:
|
||||||
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_pull(image_name: str) -> tuple:
|
||||||
|
"""Run docker pull and stream output in real time.
|
||||||
|
|
||||||
|
Returns (returncode, last_lines_of_output).
|
||||||
|
"""
|
||||||
|
process = subprocess.Popen(
|
||||||
|
['docker', 'pull', image_name],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if process.stdout:
|
||||||
|
for line in process.stdout:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
lines.append(line)
|
||||||
|
_log(f' {line}')
|
||||||
|
# Keep only the last 50 lines for error reporting
|
||||||
|
if len(lines) > 50:
|
||||||
|
lines.pop(0)
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
return process.returncode, '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def pull_one_image(args_tuple, state: dict, output_dir: Path, local_images: set):
|
||||||
|
"""Pull a single image and update shared state.
|
||||||
|
|
||||||
|
Returns (idx, image_name, status, message).
|
||||||
|
"""
|
||||||
|
idx, total, instance, namespace, arch = args_tuple
|
||||||
|
image_name = get_image_name(instance, namespace, arch)
|
||||||
|
if not image_name:
|
||||||
|
return idx, None, 'skip', 'missing instance_id'
|
||||||
|
|
||||||
|
# Resume: skip if already recorded in state
|
||||||
|
if image_name in state['done'] or image_name in state['skipped']:
|
||||||
|
return idx, image_name, 'skip', 'already recorded in state'
|
||||||
|
|
||||||
|
# Also check local docker images (in case state was deleted)
|
||||||
|
if image_name in local_images:
|
||||||
|
state['skipped'].append(image_name)
|
||||||
|
save_state(output_dir, state)
|
||||||
|
return idx, image_name, 'skip', 'already exists locally'
|
||||||
|
|
||||||
|
_log(f'⬇️ [{idx+1}/{total}] START {image_name}')
|
||||||
|
|
||||||
|
rc, output = _stream_pull(image_name)
|
||||||
|
|
||||||
|
if rc == 0:
|
||||||
|
state['done'].append(image_name)
|
||||||
|
save_state(output_dir, state)
|
||||||
|
return idx, image_name, 'success', ''
|
||||||
|
else:
|
||||||
|
msg = output.strip()[-500:] if output.strip() else 'docker pull failed'
|
||||||
|
state['failed'].append(image_name)
|
||||||
|
save_state(output_dir, state)
|
||||||
|
return idx, image_name, 'fail', msg
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Pre-pull SWE-bench Docker images with resume support')
|
||||||
|
parser.add_argument(
|
||||||
|
'--dataset',
|
||||||
|
default='swe_bench_verified',
|
||||||
|
choices=list(DATASET_IDS.keys()),
|
||||||
|
help='SWE-bench dataset variant',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--max-workers',
|
||||||
|
type=int,
|
||||||
|
default=4,
|
||||||
|
help='Max concurrent docker pulls (default: 4)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--force-arch',
|
||||||
|
default='x86_64',
|
||||||
|
choices=['', 'arm64', 'x86_64'],
|
||||||
|
help='Image architecture (default: x86_64)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dockerhub-username',
|
||||||
|
default='swebench',
|
||||||
|
help='DockerHub namespace for remote images (default: swebench)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-dir',
|
||||||
|
default=str(DEFAULT_OUTPUT_DIR),
|
||||||
|
help='Directory to store progress state (default: output/)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help='Only test the first N images',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--retry-failed',
|
||||||
|
action='store_true',
|
||||||
|
help='Retry images that failed in previous runs',
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
output_dir = Path(args.output_dir)
|
||||||
|
state = load_state(output_dir)
|
||||||
|
|
||||||
|
# Optionally retry previously failed images
|
||||||
|
if args.retry_failed:
|
||||||
|
failed = state['failed']
|
||||||
|
state['failed'] = []
|
||||||
|
print(f'🔄 重试 {len(failed)} 个之前失败的镜像')
|
||||||
|
|
||||||
|
dataset_id = DATASET_IDS[args.dataset]
|
||||||
|
samples = load_swe_samples(dataset_id)
|
||||||
|
|
||||||
|
if args.dry_run > 0:
|
||||||
|
samples = samples[:args.dry_run]
|
||||||
|
print(f'⚠️ 测试模式:仅处理前 {args.dry_run} 个样本以验证镜像名和网络...')
|
||||||
|
else:
|
||||||
|
print(f'✅ 成功加载 {len(samples)} 个样本。')
|
||||||
|
|
||||||
|
already_done = len(state['done']) + len(state['skipped'])
|
||||||
|
print(f'📂 进度文件: {get_state_path(output_dir)}')
|
||||||
|
print(f' 已下载/已存在: {len(state["done"])} | 已跳过: {len(state["skipped"])} | 历史失败: {len(state["failed"])}')
|
||||||
|
|
||||||
|
# Cache local image names once to avoid 500+ `docker images -q` calls.
|
||||||
|
print('🔄 正在扫描本地镜像...')
|
||||||
|
local_images = get_local_images()
|
||||||
|
print(f' 本地已有 {len(local_images)} 个镜像')
|
||||||
|
print('-' * 60)
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
(idx, len(samples), sample, args.dockerhub_username, args.force_arch)
|
||||||
|
for idx, sample in enumerate(samples)
|
||||||
|
]
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
skip_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=args.max_workers) as executor:
|
||||||
|
futures = {executor.submit(pull_one_image, t, state, output_dir, local_images): t[0] for t in tasks}
|
||||||
|
for future in tqdm(as_completed(futures), total=len(futures), desc='Pulling images', unit='img', mininterval=1.0):
|
||||||
|
idx = futures[future]
|
||||||
|
try:
|
||||||
|
_, image_name, status, msg = future.result()
|
||||||
|
except Exception as e:
|
||||||
|
image_name, status, msg = None, 'fail', str(e)
|
||||||
|
|
||||||
|
if status == 'success':
|
||||||
|
success_count += 1
|
||||||
|
_log(f'✅ [{idx+1}/{len(samples)}] DONE {image_name}')
|
||||||
|
elif status == 'skip':
|
||||||
|
skip_count += 1
|
||||||
|
_log(f'⏭️ [{idx+1}/{len(samples)}] SKIP {image_name or msg}')
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
_log(f'❌ [{idx+1}/{len(samples)}] FAILED {image_name}')
|
||||||
|
if msg:
|
||||||
|
_log(f' {msg}')
|
||||||
|
|
||||||
|
print('=' * 60)
|
||||||
|
print('🎉 预拉取任务结束!')
|
||||||
|
print(f' 总计: {len(samples)} | 已存在/跳过: {skip_count} | 本次成功: {success_count} | 本次失败: {fail_count}')
|
||||||
|
print(f' 进度文件: {get_state_path(output_dir)}')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('\n\n⚠️ 用户手动中断。进度已保存,下次运行会自动跳过已完成的镜像。')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
156
bash/run.py
156
bash/run.py
@ -31,6 +31,7 @@ Examples:
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -43,6 +44,11 @@ from evalscope.config import SandboxTaskConfig
|
|||||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||||
|
|
||||||
|
# Make collect_results importable
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
import collect_results as collect_results_module
|
||||||
|
import perf_backup as perf_backup_module
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Default configuration (override via CLI)
|
# Default configuration (override via CLI)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@ -231,6 +237,10 @@ def build_parser():
|
|||||||
parser.add_argument('--truncation-tokens', type=int, default=DEFAULT_TRUNCATION_TOKENS,
|
parser.add_argument('--truncation-tokens', type=int, default=DEFAULT_TRUNCATION_TOKENS,
|
||||||
help='Middle-truncation token budget for long-context benchmarks (default: %(default)s)')
|
help='Middle-truncation token budget for long-context benchmarks (default: %(default)s)')
|
||||||
|
|
||||||
|
# Result collection
|
||||||
|
parser.add_argument('--no-summary', dest='write_summary', action='store_false',
|
||||||
|
help='Skip writing summary Excel/CSV after each benchmark')
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@ -433,6 +443,95 @@ def build_task_config(
|
|||||||
# Main
|
# Main
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
def write_summary(output_dir: str, model_name: str,
|
||||||
|
benchmark_names: list = None):
|
||||||
|
"""Re-aggregate results for the given benchmarks (or all on disk if None)."""
|
||||||
|
try:
|
||||||
|
# Derive a safe file name from the model name
|
||||||
|
safe_name = model_name.replace('/', '_').replace('\\', '_').replace(' ', '_')
|
||||||
|
excel_output_dir = PROJECT_ROOT / 'results'
|
||||||
|
if benchmark_names is not None:
|
||||||
|
collect_results_module.eval_benchmark(
|
||||||
|
benchmark_names, Path(output_dir), model_name, safe_name,
|
||||||
|
excel_output_dir=excel_output_dir,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
collect_results_module.collect_all(
|
||||||
|
Path(output_dir), model_name, safe_name,
|
||||||
|
excel_output_dir=excel_output_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: failed to write summary: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def run_and_summarize(task_cfg, write_summary_flag: bool, output_dir: str,
|
||||||
|
model_name: str, benchmark_names: list):
|
||||||
|
"""Run a benchmark task and optionally refresh the summary table.
|
||||||
|
|
||||||
|
``benchmark_names`` is the running list of canonical benchmark names that
|
||||||
|
were scheduled for this invocation. The summary table will be limited to
|
||||||
|
these benchmarks so it does not pick up older results left in the output
|
||||||
|
directory from previous runs.
|
||||||
|
|
||||||
|
After a successful ``run_task()`` we also snapshot the cumulative
|
||||||
|
``perf_metrics.summary`` and the per-sample predictions into the durable
|
||||||
|
backup files maintained by ``perf_backup.py`` so checkpoint restarts can
|
||||||
|
recover them.
|
||||||
|
"""
|
||||||
|
dataset_name = task_cfg.datasets[0]
|
||||||
|
work_dir = Path(task_cfg.work_dir)
|
||||||
|
restore_before_run(output_dir, dataset_name, model_name, work_dir)
|
||||||
|
|
||||||
|
start_ts = time.monotonic()
|
||||||
|
try:
|
||||||
|
run_task(task_cfg)
|
||||||
|
finally:
|
||||||
|
elapsed = time.monotonic() - start_ts
|
||||||
|
perf_backup_module.record_active_time(output_dir, dataset_name, model_name, elapsed)
|
||||||
|
print(f'Active time for {dataset_name}: {elapsed:.1f}s (total accumulated)')
|
||||||
|
|
||||||
|
backup_after_run(output_dir, dataset_name, model_name, work_dir)
|
||||||
|
if write_summary_flag:
|
||||||
|
write_summary(output_dir, model_name, benchmark_names=benchmark_names)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_after_run(output_dir: str, benchmark: str, model_name: str,
|
||||||
|
work_dir: Path):
|
||||||
|
"""Snapshot the just-finished run's perf summary and predictions."""
|
||||||
|
try:
|
||||||
|
report_json = work_dir / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
perf_backup_module.backup_perf_stats(
|
||||||
|
Path(output_dir), benchmark, model_name, report_json,
|
||||||
|
)
|
||||||
|
predictions_dir = work_dir / 'predictions' / model_name
|
||||||
|
perf_backup_module.archive_predictions(
|
||||||
|
Path(output_dir), benchmark, model_name, predictions_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: perf backup for {benchmark} failed: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def restore_before_run(output_dir: str, benchmark: str, model_name: str,
|
||||||
|
work_dir: Path) -> bool:
|
||||||
|
"""If durable backups exist for a benchmark/model, materialise them into
|
||||||
|
the new ``work_dir`` before evalscope starts so the next run resumes from
|
||||||
|
the larger historical state instead of overwriting it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
predictions_dir = work_dir / 'predictions' / model_name
|
||||||
|
report_json = work_dir / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
restored = perf_backup_module.restore_from_backup(
|
||||||
|
Path(output_dir), benchmark, model_name,
|
||||||
|
predictions_dir, report_json,
|
||||||
|
)
|
||||||
|
if restored:
|
||||||
|
print(f'Restored {benchmark}/{model_name} from backup before run')
|
||||||
|
return restored
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: perf restore for {benchmark} failed: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -499,65 +598,60 @@ def main():
|
|||||||
print(f'Multi-run datasets: {multi_run}')
|
print(f'Multi-run datasets: {multi_run}')
|
||||||
print(f'Single-run datasets: {single_run}')
|
print(f'Single-run datasets: {single_run}')
|
||||||
print(f'Agent datasets: {agent}')
|
print(f'Agent datasets: {agent}')
|
||||||
|
print(f'Write summary: {args.write_summary}')
|
||||||
print('=' * 60)
|
print('=' * 60)
|
||||||
|
|
||||||
|
def run_one(dataset_name, run_idx=0, benchmark_names=None):
|
||||||
|
ds_cfg = dataset_configs[dataset_name]
|
||||||
|
task_cfg = build_task_config(
|
||||||
|
dataset_name, ds_cfg, args.batch_size, enable_thinking, args.seed, limit,
|
||||||
|
args.output_dir, args.model, args.api_url, args.dataset_dir, judge_model_args,
|
||||||
|
run_idx=run_idx,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
run_and_summarize(task_cfg, args.write_summary, args.output_dir, args.model,
|
||||||
|
benchmark_names=benchmark_names)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'ERROR in {dataset_name} (run {run_idx + 1 if run_idx else 1}): {e}')
|
||||||
|
|
||||||
|
# Keep the ordered list of benchmarks actually run in this invocation so
|
||||||
|
# the summary table only includes them, not stale results from earlier runs.
|
||||||
|
benchmark_names = []
|
||||||
|
|
||||||
for dataset_name in multi_run:
|
for dataset_name in multi_run:
|
||||||
if dataset_name not in dataset_configs:
|
if dataset_name not in dataset_configs:
|
||||||
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
continue
|
continue
|
||||||
ds_cfg = dataset_configs[dataset_name]
|
benchmark_names.append(dataset_name)
|
||||||
num_runs = MULTI_RUN_CONFIG.get(dataset_name, 1)
|
num_runs = MULTI_RUN_CONFIG.get(dataset_name, 1)
|
||||||
for run_idx in range(num_runs):
|
for run_idx in range(num_runs):
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f'Running: {dataset_name} (run {run_idx + 1}/{num_runs}, seed={args.seed})')
|
print(f'Running: {dataset_name} (run {run_idx + 1}/{num_runs}, seed={args.seed})')
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
task_cfg = build_task_config(
|
run_one(dataset_name, run_idx=run_idx, benchmark_names=benchmark_names)
|
||||||
dataset_name, ds_cfg, args.batch_size, enable_thinking, args.seed, limit,
|
|
||||||
args.output_dir, args.model, args.api_url, args.dataset_dir, judge_model_args,
|
|
||||||
run_idx=run_idx,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
run_task(task_cfg)
|
|
||||||
except Exception as e:
|
|
||||||
print(f'ERROR in {dataset_name} (run {run_idx + 1}): {e}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
for dataset_name in single_run:
|
for dataset_name in single_run:
|
||||||
if dataset_name not in dataset_configs:
|
if dataset_name not in dataset_configs:
|
||||||
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
continue
|
continue
|
||||||
ds_cfg = dataset_configs[dataset_name]
|
benchmark_names.append(dataset_name)
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f'Running: {dataset_name} (seed={args.seed})')
|
print(f'Running: {dataset_name} (seed={args.seed})')
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
task_cfg = build_task_config(
|
run_one(dataset_name, benchmark_names=benchmark_names)
|
||||||
dataset_name, ds_cfg, args.batch_size, enable_thinking, args.seed, limit,
|
|
||||||
args.output_dir, args.model, args.api_url, args.dataset_dir, judge_model_args,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
run_task(task_cfg)
|
|
||||||
except Exception as e:
|
|
||||||
print(f'ERROR in {dataset_name}: {e}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
for dataset_name in agent:
|
for dataset_name in agent:
|
||||||
if dataset_name not in dataset_configs:
|
if dataset_name not in dataset_configs:
|
||||||
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
continue
|
continue
|
||||||
ds_cfg = dataset_configs[dataset_name]
|
benchmark_names.append(dataset_name)
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f'Running: {dataset_name} (seed={args.seed})')
|
print(f'Running: {dataset_name} (seed={args.seed})')
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
task_cfg = build_task_config(
|
run_one(dataset_name, benchmark_names=benchmark_names)
|
||||||
dataset_name, ds_cfg, args.batch_size, enable_thinking, args.seed, limit,
|
|
||||||
args.output_dir, args.model, args.api_url, args.dataset_dir, judge_model_args,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
run_task(task_cfg)
|
|
||||||
except Exception as e:
|
|
||||||
print(f'ERROR in {dataset_name}: {e}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
if args.write_summary:
|
||||||
|
write_summary(args.output_dir, args.model, benchmark_names=benchmark_names)
|
||||||
print('\nAll benchmarks done!')
|
print('\nAll benchmarks done!')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
bash/set.sh
Normal file
9
bash/set.sh
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
docker run -it --rm \
|
||||||
|
--network host \
|
||||||
|
-v /data1/sora/evalscope:/opt/evalscope \
|
||||||
|
-v /data1/sora/models:/opt/models \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
evalscope-complete-py312:latest \
|
||||||
|
bash
|
||||||
|
|
||||||
|
|
||||||
659
bash/test.py
Normal file
659
bash/test.py
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Unified benchmark runner for EvalScope.
|
||||||
|
|
||||||
|
A single entry point for lite / mid / full / group1 / group2 / group3 evaluations.
|
||||||
|
All tunable parameters can be controlled via command-line arguments.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Full evaluation (all benchmarks, multi-run for stability)
|
||||||
|
python bash/run.py \
|
||||||
|
--model DeepSeek-V4-Flash-Int8 \
|
||||||
|
--api-url http://localhost:30000/v1 \
|
||||||
|
--dataset-dir /data1/sora/evalscope \
|
||||||
|
--output-dir /data1/sora/evalscope/output \
|
||||||
|
--suite full \
|
||||||
|
--limit none
|
||||||
|
|
||||||
|
# Lite smoke test (~5h with full samples)
|
||||||
|
python bash/run.py --suite lite --limit none
|
||||||
|
|
||||||
|
# Run only selected benchmarks
|
||||||
|
python bash/run.py --datasets aime24,gsm8k,arc --limit 20
|
||||||
|
|
||||||
|
# Custom judge model
|
||||||
|
python bash/run.py \
|
||||||
|
--judge-model deepseek-v4-pro \
|
||||||
|
--judge-api-url https://api.deepseek.com/v1 \
|
||||||
|
--judge-api-key sk-xxx
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from evalscope import run_task, TaskConfig
|
||||||
|
from evalscope.api.agent import NativeAgentConfig
|
||||||
|
from evalscope.config import SandboxTaskConfig
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
|
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||||
|
|
||||||
|
# Make collect_results importable
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
import collect_results as collect_results_module
|
||||||
|
import perf_backup as perf_backup_module
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Default configuration (override via CLI)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
DEFAULT_MODEL = 'DeepSeek-V4-Flash-Int8'
|
||||||
|
DEFAULT_API_URL = 'http://localhost:30000/v1'
|
||||||
|
DEFAULT_DATASET_DIR = str(PROJECT_ROOT)
|
||||||
|
DEFAULT_OUTPUT_DIR = str(PROJECT_ROOT / 'output')
|
||||||
|
DEFAULT_CONFIG = str(PROJECT_ROOT / 'config' / 'dpv4-int8_nothinking.yaml')
|
||||||
|
DEFAULT_TOKENIZER_PATH = '/data1/models/DeepSeek-V4-Flash-INT8'
|
||||||
|
DEFAULT_LIMIT = None
|
||||||
|
DEFAULT_SEED = 42
|
||||||
|
DEFAULT_BATCH_SIZE = 4
|
||||||
|
DEFAULT_ENABLE_THINKING = False
|
||||||
|
|
||||||
|
DEFAULT_JUDGE_MODEL = 'DeepSeek/DeepSeek-V4-Pro'
|
||||||
|
DEFAULT_JUDGE_API_URL = 'https://api.vectron.meta-stone.com/v1'
|
||||||
|
DEFAULT_JUDGE_API_KEY = 'sk-dbd8a665f7634081b87ec409c7636500'
|
||||||
|
DEFAULT_JUDGE_MAX_TOKENS = 10240
|
||||||
|
|
||||||
|
# 长文本 middle-truncation 上限(token 数)。当前默认 128k。
|
||||||
|
DEFAULT_TRUNCATION_TOKENS = 32768 * 4
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Benchmark suites
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 多次采样配置:总样本数控制在 ~400-500
|
||||||
|
MULTI_RUN_CONFIG = {
|
||||||
|
'aime24': 12,
|
||||||
|
'aime25': 12,
|
||||||
|
'aime26': 12,
|
||||||
|
'hmmt26': 12,
|
||||||
|
'live_code_bench': 5,
|
||||||
|
'imo_answerbench': 4,
|
||||||
|
'humaneval': 3,
|
||||||
|
'gpqa_diamond': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 能力域完整列表
|
||||||
|
ALL_MULTI_RUN = [
|
||||||
|
# 'humaneval', 'live_code_bench',
|
||||||
|
# 'aime24', 'aime25', 'aime26', 'hmmt26',
|
||||||
|
# 'imo_answerbench', 'gpqa_diamond',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
ALL_SINGLE_RUN = [
|
||||||
|
# 'bigcodebench', 'bfcl_v3', 'competition_math', 'gsm8k', 'hle', 'super_gpqa',
|
||||||
|
# 'arc', 'bbh', 'cmmlu', 'drop', 'hellaswag', 'mmlu', 'mmlu_pro',
|
||||||
|
# 'simple_qa', 'trivia_qa', 'winogrande',
|
||||||
|
# 'openai_mrcr', 'longbench_v2',
|
||||||
|
'swe_bench_verified', 'swe_bench_pro',
|
||||||
|
]
|
||||||
|
ALL_AGENT = [
|
||||||
|
# 'tau2_bench', 'general_fc'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 分组基于 CSV 单次时间 + multi-run 后的 wall time 平衡:
|
||||||
|
# Group1: ~61h | Group2: ~62h | Group3: ~55h
|
||||||
|
SUITES = {
|
||||||
|
'full': {
|
||||||
|
'multi': ALL_MULTI_RUN,
|
||||||
|
'single': ALL_SINGLE_RUN,
|
||||||
|
'agent': ALL_AGENT,
|
||||||
|
},
|
||||||
|
'lite': {
|
||||||
|
'multi': ['aime24', 'humaneval'],
|
||||||
|
'single': ['gsm8k', 'arc', 'longbench_v2'],
|
||||||
|
'agent': ['general_fc'],
|
||||||
|
},
|
||||||
|
'mid': {
|
||||||
|
'multi': ['aime24', 'humaneval'],
|
||||||
|
'single': [
|
||||||
|
'live_code_bench', 'bigcodebench', 'competition_math', 'gsm8k',
|
||||||
|
'gpqa_diamond', 'mmlu_pro', 'simple_qa', 'longbench_v2', 'openai_mrcr',
|
||||||
|
],
|
||||||
|
'agent': ['general_fc', 'tau2_bench'],
|
||||||
|
},
|
||||||
|
# 多机组分组,基于 CSV 实测完整时间(已含 multi-run)平衡:
|
||||||
|
# Group1: ~22.7h | Group2: ~25.6h | Group3: ~27.0h | 合计 ~75.2h
|
||||||
|
'group1': {
|
||||||
|
'multi': ['live_code_bench', 'aime24', 'aime25', 'aime26', 'hmmt26', 'imo_answerbench', 'humaneval'],
|
||||||
|
'single': ['bigcodebench', 'competition_math', 'gsm8k', 'drop', 'arc', 'hellaswag', 'winogrande'],
|
||||||
|
'agent': [],
|
||||||
|
},
|
||||||
|
'group2': {
|
||||||
|
'multi': [],
|
||||||
|
'single': ['hle', 'mmlu_pro', 'trivia_qa'],
|
||||||
|
'agent': [],
|
||||||
|
},
|
||||||
|
'group3': {
|
||||||
|
'multi': ['gpqa_diamond'],
|
||||||
|
'single': ['openai_mrcr', 'longbench_v2', 'bfcl_v3', 'mmlu', 'cmmlu', 'bbh', 'simple_qa'],
|
||||||
|
'agent': ['tau2_bench', 'general_fc'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Fixed configuration
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
MATH_DATASETS = {
|
||||||
|
'aime24', 'aime25', 'aime26', 'hmmt26',
|
||||||
|
'gsm8k', 'competition_math', 'imo_answerbench',
|
||||||
|
}
|
||||||
|
|
||||||
|
MATH_PROMPT_TEMPLATE = (
|
||||||
|
"{question}\n"
|
||||||
|
"Please reason step by step, and put your final answer within \\boxed{{}}."
|
||||||
|
)
|
||||||
|
|
||||||
|
SANDBOX_DATASETS = {'humaneval', 'bigcodebench', 'swe_bench_verified', 'swe_bench_pro'}
|
||||||
|
SANDBOX_CONFIGS = {
|
||||||
|
'bigcodebench': {
|
||||||
|
'image': 'bigcodebench-sandbox:latest',
|
||||||
|
'working_dir': '/tmp',
|
||||||
|
'tools_config': {
|
||||||
|
'shell_executor': {},
|
||||||
|
'python_executor': {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'humaneval': {
|
||||||
|
'image': 'python:3.11-slim',
|
||||||
|
'tools_config': {
|
||||||
|
'shell_executor': {},
|
||||||
|
'python_executor': {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'swe_bench_verified': {
|
||||||
|
'image': 'swe-bench-sandbox:latest',
|
||||||
|
'working_dir': '/tmp',
|
||||||
|
'tools_config': {
|
||||||
|
'shell_executor': {},
|
||||||
|
'python_executor': {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'swe_bench_pro': {
|
||||||
|
'image': 'swe-bench-sandbox:latest',
|
||||||
|
'working_dir': '/tmp',
|
||||||
|
'tools_config': {
|
||||||
|
'shell_executor': {},
|
||||||
|
'python_executor': {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CLI parser
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def build_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Unified EvalScope benchmark runner',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog='Suites: full, lite, mid, group1, group2, group3',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model / API
|
||||||
|
parser.add_argument('--model', default=DEFAULT_MODEL,
|
||||||
|
help='Served model name (default: %(default)s)')
|
||||||
|
parser.add_argument('--api-url', default=DEFAULT_API_URL,
|
||||||
|
help='OpenAI-compatible API URL (default: %(default)s)')
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
parser.add_argument('--dataset-dir', default=DEFAULT_DATASET_DIR,
|
||||||
|
help='Parent directory containing datasets/ subdir (default: %(default)s)')
|
||||||
|
parser.add_argument('--output-dir', default=DEFAULT_OUTPUT_DIR,
|
||||||
|
help='Output root directory (default: %(default)s)')
|
||||||
|
parser.add_argument('--config', default=DEFAULT_CONFIG,
|
||||||
|
help='YAML config path (default: %(default)s)')
|
||||||
|
parser.add_argument('--tokenizer-path', default=DEFAULT_TOKENIZER_PATH,
|
||||||
|
help='Local tokenizer path for middle-truncation (default: %(default)s)')
|
||||||
|
|
||||||
|
# Run control
|
||||||
|
parser.add_argument('--suite', default='full', choices=list(SUITES.keys()),
|
||||||
|
help='Benchmark suite to run (default: %(default)s)')
|
||||||
|
parser.add_argument('--datasets', '--benchmarks', dest='datasets', default=None,
|
||||||
|
help='Override suite with comma-separated benchmark names, e.g. aime24,gsm8k')
|
||||||
|
parser.add_argument('--exclude', default=None,
|
||||||
|
help='Comma-separated benchmarks to exclude from the chosen suite')
|
||||||
|
parser.add_argument('--limit', default=None,
|
||||||
|
help='Max samples per benchmark; "none"/"all" for no limit (default: none)')
|
||||||
|
parser.add_argument('--seed', type=int, default=DEFAULT_SEED,
|
||||||
|
help='Random seed (default: %(default)s)')
|
||||||
|
parser.add_argument('--batch-size', type=int, default=DEFAULT_BATCH_SIZE,
|
||||||
|
help='Evaluation batch size (default: %(default)s)')
|
||||||
|
|
||||||
|
# Decoding / thinking
|
||||||
|
parser.add_argument('--thinking', action='store_true', default=None,
|
||||||
|
help='Enable thinking mode (sglang chat_template_kwargs.thinking=True)')
|
||||||
|
parser.add_argument('--no-thinking', dest='thinking', action='store_false',
|
||||||
|
help='Disable thinking mode (default)')
|
||||||
|
|
||||||
|
# Judge model
|
||||||
|
parser.add_argument('--judge-model', default=DEFAULT_JUDGE_MODEL,
|
||||||
|
help='Judge model name (default: %(default)s)')
|
||||||
|
parser.add_argument('--judge-api-url', default=DEFAULT_JUDGE_API_URL,
|
||||||
|
help='Judge model API URL (default: %(default)s)')
|
||||||
|
parser.add_argument('--judge-api-key', default=DEFAULT_JUDGE_API_KEY,
|
||||||
|
help='Judge model API key')
|
||||||
|
parser.add_argument('--judge-max-tokens', type=int, default=DEFAULT_JUDGE_MAX_TOKENS,
|
||||||
|
help='Judge model max_tokens (default: %(default)s)')
|
||||||
|
|
||||||
|
# Truncation
|
||||||
|
parser.add_argument('--truncation-tokens', type=int, default=DEFAULT_TRUNCATION_TOKENS,
|
||||||
|
help='Middle-truncation token budget for long-context benchmarks (default: %(default)s)')
|
||||||
|
|
||||||
|
# Result collection
|
||||||
|
parser.add_argument('--no-summary', dest='write_summary', action='store_false',
|
||||||
|
help='Skip writing summary Excel/CSV after each benchmark')
|
||||||
|
parser.add_argument('--summary-name', default=None,
|
||||||
|
help='Output summary file name (without extension); defaults to safe model name')
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Middle-truncation helpers
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
_TOKENIZER = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_tokenizer(tokenizer_path: str):
|
||||||
|
global _TOKENIZER
|
||||||
|
if _TOKENIZER is None:
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
try:
|
||||||
|
_TOKENIZER = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
|
||||||
|
except Exception:
|
||||||
|
_TOKENIZER = AutoTokenizer.from_pretrained('deepseek-ai/DeepSeek-V4-Flash', trust_remote_code=True)
|
||||||
|
return _TOKENIZER
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_middle(text: str, max_tokens: int, tokenizer_path: str) -> str:
|
||||||
|
if max_tokens <= 0:
|
||||||
|
return text
|
||||||
|
tokenizer = get_tokenizer(tokenizer_path)
|
||||||
|
token_ids = tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
if len(token_ids) <= max_tokens:
|
||||||
|
return text
|
||||||
|
keep_head = max_tokens // 2
|
||||||
|
keep_tail = max_tokens - keep_head
|
||||||
|
truncated_ids = token_ids[:keep_head] + token_ids[-keep_tail:]
|
||||||
|
return tokenizer.decode(truncated_ids, skip_special_tokens=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_adapters_for_truncation(tokenizer_path: str, truncation_tokens: int):
|
||||||
|
from evalscope.benchmarks.longbench_v2.longbench_v2_adapter import LongBenchV2Adapter
|
||||||
|
from evalscope.benchmarks.openai_mrcr.openai_mrcr_adapter import OpenAIMRCRAdapter
|
||||||
|
|
||||||
|
_orig_longbench_format = LongBenchV2Adapter.format_prompt_template
|
||||||
|
|
||||||
|
def _patched_longbench_format(self, sample):
|
||||||
|
if sample.metadata and 'context' in sample.metadata:
|
||||||
|
sample.metadata['context'] = truncate_middle(sample.metadata['context'], truncation_tokens, tokenizer_path)
|
||||||
|
return _orig_longbench_format(self, sample)
|
||||||
|
|
||||||
|
LongBenchV2Adapter.format_prompt_template = _patched_longbench_format
|
||||||
|
|
||||||
|
_orig_mrcr_record = OpenAIMRCRAdapter.record_to_sample
|
||||||
|
|
||||||
|
def _patched_mrcr_record(self, record):
|
||||||
|
per_msg_max_tok = 8192
|
||||||
|
if 'prompt' in record:
|
||||||
|
try:
|
||||||
|
prompt_data = json.loads(record['prompt'])
|
||||||
|
if not isinstance(prompt_data, list) or len(prompt_data) == 0:
|
||||||
|
return _orig_mrcr_record(self, record)
|
||||||
|
|
||||||
|
tokenizer = get_tokenizer(tokenizer_path)
|
||||||
|
total_tok = sum(
|
||||||
|
len(tokenizer.encode(msg.get('content', '') if isinstance(msg, dict) else '', add_special_tokens=False))
|
||||||
|
for msg in prompt_data
|
||||||
|
)
|
||||||
|
if total_tok <= truncation_tokens:
|
||||||
|
return _orig_mrcr_record(self, record)
|
||||||
|
|
||||||
|
desired_idx = record.get('desired_msg_index', 0)
|
||||||
|
if not isinstance(desired_idx, int) or desired_idx < 0 or desired_idx >= len(prompt_data):
|
||||||
|
desired_idx = 0
|
||||||
|
|
||||||
|
n = len(prompt_data)
|
||||||
|
keep = set()
|
||||||
|
keep.update(range(min(2, n)))
|
||||||
|
keep.update(range(max(0, n - 2), n))
|
||||||
|
window = 2
|
||||||
|
keep.update(range(max(0, desired_idx - window), min(n, desired_idx + window + 1)))
|
||||||
|
keep = sorted(keep)
|
||||||
|
|
||||||
|
new_prompt = []
|
||||||
|
for idx in keep:
|
||||||
|
msg = prompt_data[idx]
|
||||||
|
if isinstance(msg, dict):
|
||||||
|
msg = dict(msg)
|
||||||
|
content = msg.get('content', '')
|
||||||
|
if len(tokenizer.encode(content, add_special_tokens=False)) > per_msg_max_tok:
|
||||||
|
msg['content'] = truncate_middle(content, per_msg_max_tok, tokenizer_path)
|
||||||
|
new_prompt.append(msg)
|
||||||
|
|
||||||
|
record = dict(record)
|
||||||
|
record['prompt'] = json.dumps(new_prompt)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
return _orig_mrcr_record(self, record)
|
||||||
|
|
||||||
|
OpenAIMRCRAdapter.record_to_sample = _patched_mrcr_record
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Helpers
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def load_dataset_configs(config_path: str):
|
||||||
|
if not Path(config_path).exists():
|
||||||
|
raise FileNotFoundError(f'Config file not found: {config_path}')
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_thinking(generation_config: dict, enable: bool) -> dict:
|
||||||
|
extra_body = generation_config.get('extra_body', {})
|
||||||
|
chat_template_kwargs = extra_body.get('chat_template_kwargs', {})
|
||||||
|
if enable:
|
||||||
|
chat_template_kwargs['thinking'] = True
|
||||||
|
else:
|
||||||
|
chat_template_kwargs.pop('thinking', None)
|
||||||
|
if chat_template_kwargs:
|
||||||
|
extra_body['chat_template_kwargs'] = chat_template_kwargs
|
||||||
|
if extra_body:
|
||||||
|
generation_config['extra_body'] = extra_body
|
||||||
|
return generation_config
|
||||||
|
|
||||||
|
|
||||||
|
def build_agent_config(agent_cfg: dict) -> NativeAgentConfig:
|
||||||
|
agent_cfg = deepcopy(agent_cfg or {})
|
||||||
|
known_fields = {'mode', 'strategy', 'tools', 'max_steps', 'mcp_servers', 'environment', 'environment_extra'}
|
||||||
|
kwargs = agent_cfg.pop('kwargs', {})
|
||||||
|
for key in list(agent_cfg.keys()):
|
||||||
|
if key not in known_fields:
|
||||||
|
kwargs[key] = agent_cfg.pop(key)
|
||||||
|
if kwargs:
|
||||||
|
agent_cfg['kwargs'] = kwargs
|
||||||
|
return NativeAgentConfig(**agent_cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def build_task_config(
|
||||||
|
dataset_name: str,
|
||||||
|
ds_cfg: dict,
|
||||||
|
batch_size: int,
|
||||||
|
enable_thinking: bool,
|
||||||
|
seed: int,
|
||||||
|
limit,
|
||||||
|
output_dir: str,
|
||||||
|
model: str,
|
||||||
|
api_url: str,
|
||||||
|
dataset_dir: str,
|
||||||
|
judge_model_args: dict,
|
||||||
|
run_idx: int = 0,
|
||||||
|
) -> TaskConfig:
|
||||||
|
if run_idx > 0:
|
||||||
|
work_dir = Path(output_dir) / dataset_name / f'seed_{seed}_run_{run_idx}'
|
||||||
|
else:
|
||||||
|
work_dir = Path(output_dir) / dataset_name / f'seed_{seed}'
|
||||||
|
work_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
work_dir = str(work_dir)
|
||||||
|
|
||||||
|
generation_config = configure_thinking(deepcopy(ds_cfg['generation_config']), enable_thinking)
|
||||||
|
dataset_args = deepcopy(ds_cfg.get('dataset_args', {}))
|
||||||
|
dataset_args.setdefault('shuffle', True)
|
||||||
|
|
||||||
|
if dataset_name in MATH_DATASETS:
|
||||||
|
dataset_args['prompt_template'] = MATH_PROMPT_TEMPLATE
|
||||||
|
|
||||||
|
dataset_args_dict = {dataset_name: dataset_args}
|
||||||
|
|
||||||
|
agent_config = None
|
||||||
|
if 'agent_config' in ds_cfg:
|
||||||
|
agent_config = build_agent_config(ds_cfg['agent_config'])
|
||||||
|
|
||||||
|
return TaskConfig(
|
||||||
|
model=model,
|
||||||
|
api_url=api_url,
|
||||||
|
eval_type='openai_api',
|
||||||
|
dataset_dir=dataset_dir,
|
||||||
|
judge_model_args=judge_model_args,
|
||||||
|
seed=seed,
|
||||||
|
limit=limit,
|
||||||
|
collect_perf=True,
|
||||||
|
no_timestamp=True,
|
||||||
|
work_dir=work_dir,
|
||||||
|
use_cache=work_dir,
|
||||||
|
datasets=[dataset_name],
|
||||||
|
generation_config=generation_config,
|
||||||
|
dataset_args=dataset_args_dict,
|
||||||
|
agent_config=agent_config,
|
||||||
|
eval_batch_size=batch_size,
|
||||||
|
sandbox=SandboxTaskConfig(
|
||||||
|
enabled=True,
|
||||||
|
engine='docker',
|
||||||
|
default_config=SANDBOX_CONFIGS.get(dataset_name, {
|
||||||
|
'image': 'python:3.11-slim',
|
||||||
|
'tools_config': {
|
||||||
|
'shell_executor': {},
|
||||||
|
'python_executor': {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) if dataset_name in SANDBOX_DATASETS else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def write_summary(output_dir: str, model_name: str, summary_name: str):
|
||||||
|
"""Re-aggregate results across all benchmarks under output_dir."""
|
||||||
|
try:
|
||||||
|
excel_output_dir = PROJECT_ROOT / 'results'
|
||||||
|
collect_results_module.collect_all(
|
||||||
|
Path(output_dir), model_name, summary_name,
|
||||||
|
excel_output_dir=excel_output_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: failed to write summary: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def backup_after_run(output_dir: str, benchmark: str, model_name: str,
|
||||||
|
work_dir: Path):
|
||||||
|
"""Snapshot the just-finished run's perf summary and predictions."""
|
||||||
|
try:
|
||||||
|
report_json = work_dir / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
perf_backup_module.backup_perf_stats(
|
||||||
|
Path(output_dir), benchmark, model_name, report_json,
|
||||||
|
)
|
||||||
|
predictions_dir = work_dir / 'predictions' / model_name
|
||||||
|
perf_backup_module.archive_predictions(
|
||||||
|
Path(output_dir), benchmark, model_name, predictions_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: perf backup for {benchmark} failed: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
def restore_before_run(output_dir: str, benchmark: str, model_name: str,
|
||||||
|
work_dir: Path) -> bool:
|
||||||
|
"""If durable backups exist for a benchmark/model, materialise them into
|
||||||
|
the new ``work_dir`` before evalscope starts so the next run resumes from
|
||||||
|
the larger historical state instead of overwriting it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
predictions_dir = work_dir / 'predictions' / model_name
|
||||||
|
report_json = work_dir / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
restored = perf_backup_module.restore_from_backup(
|
||||||
|
Path(output_dir), benchmark, model_name,
|
||||||
|
predictions_dir, report_json,
|
||||||
|
)
|
||||||
|
if restored:
|
||||||
|
print(f'Restored {benchmark}/{model_name} from backup before run')
|
||||||
|
return restored
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: perf restore for {benchmark} failed: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_and_summarize(task_cfg, write_summary_flag: bool, output_dir: str, model_name: str, summary_name: str):
|
||||||
|
"""Run a benchmark task and optionally refresh the summary table.
|
||||||
|
|
||||||
|
After a successful ``run_task()`` we snapshot the cumulative
|
||||||
|
``perf_metrics.summary`` and the per-sample predictions into the durable
|
||||||
|
backup files maintained by ``perf_backup.py`` so checkpoint restarts can
|
||||||
|
recover them.
|
||||||
|
"""
|
||||||
|
dataset_name = task_cfg.datasets[0]
|
||||||
|
work_dir = Path(task_cfg.work_dir)
|
||||||
|
restore_before_run(output_dir, dataset_name, model_name, work_dir)
|
||||||
|
|
||||||
|
start_ts = time.monotonic()
|
||||||
|
try:
|
||||||
|
run_task(task_cfg)
|
||||||
|
finally:
|
||||||
|
elapsed = time.monotonic() - start_ts
|
||||||
|
perf_backup_module.record_active_time(output_dir, dataset_name, model_name, elapsed)
|
||||||
|
print(f'Active time for {dataset_name}: {elapsed:.1f}s (total accumulated)')
|
||||||
|
|
||||||
|
backup_after_run(output_dir, dataset_name, model_name, work_dir)
|
||||||
|
if write_summary_flag:
|
||||||
|
write_summary(output_dir, model_name, summary_name)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Resolve limit
|
||||||
|
limit = args.limit
|
||||||
|
if limit is not None:
|
||||||
|
if str(limit).lower() in ('none', 'all'):
|
||||||
|
limit = None
|
||||||
|
else:
|
||||||
|
limit = int(limit)
|
||||||
|
|
||||||
|
enable_thinking = DEFAULT_ENABLE_THINKING if args.thinking is None else args.thinking
|
||||||
|
|
||||||
|
# Resolve suite or custom datasets
|
||||||
|
if args.datasets:
|
||||||
|
custom = [d.strip() for d in args.datasets.split(',') if d.strip()]
|
||||||
|
multi_run = [d for d in custom if d in MULTI_RUN_CONFIG]
|
||||||
|
single_run = [d for d in custom if d not in MULTI_RUN_CONFIG]
|
||||||
|
agent = [d for d in custom if d in ALL_AGENT]
|
||||||
|
single_run = [d for d in single_run if d not in ALL_AGENT]
|
||||||
|
else:
|
||||||
|
suite = SUITES[args.suite]
|
||||||
|
multi_run = list(suite['multi'])
|
||||||
|
single_run = list(suite['single'])
|
||||||
|
agent = list(suite['agent'])
|
||||||
|
|
||||||
|
# Apply --exclude
|
||||||
|
if args.exclude:
|
||||||
|
exclude = {d.strip() for d in args.exclude.split(',') if d.strip()}
|
||||||
|
multi_run = [d for d in multi_run if d not in exclude]
|
||||||
|
single_run = [d for d in single_run if d not in exclude]
|
||||||
|
agent = [d for d in agent if d not in exclude]
|
||||||
|
|
||||||
|
judge_model_args = {
|
||||||
|
'model_id': args.judge_model,
|
||||||
|
'api_url': args.judge_api_url,
|
||||||
|
'api_key': args.judge_api_key,
|
||||||
|
'eval_type': 'openai_api',
|
||||||
|
'generation_config': {
|
||||||
|
'temperature': 0.0,
|
||||||
|
'max_tokens': args.judge_max_tokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
truncation_tokens = args.truncation_tokens
|
||||||
|
_patch_adapters_for_truncation(args.tokenizer_path, truncation_tokens)
|
||||||
|
|
||||||
|
dataset_configs = load_dataset_configs(args.config)
|
||||||
|
|
||||||
|
print('=' * 60)
|
||||||
|
print(f'Config: {args.config}')
|
||||||
|
print(f'Model: {args.model}')
|
||||||
|
print(f'API URL: {args.api_url}')
|
||||||
|
print(f'Dataset Dir: {args.dataset_dir}')
|
||||||
|
print(f'Output Dir: {args.output_dir}')
|
||||||
|
print(f'Suite: {args.suite}')
|
||||||
|
print(f'Limit: {limit if limit is not None else "ALL"}')
|
||||||
|
print(f'Thinking: {enable_thinking}')
|
||||||
|
print(f'Seed: {args.seed}')
|
||||||
|
print(f'Batch Size: {args.batch_size}')
|
||||||
|
print(f'Tokenizer Path: {args.tokenizer_path}')
|
||||||
|
print(f'Truncation Tokens: {truncation_tokens}')
|
||||||
|
print(f'Multi-run datasets: {multi_run}')
|
||||||
|
print(f'Single-run datasets: {single_run}')
|
||||||
|
print(f'Agent datasets: {agent}')
|
||||||
|
print(f'Write summary: {args.write_summary}')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
def run_one(dataset_name, run_idx=0):
|
||||||
|
ds_cfg = dataset_configs[dataset_name]
|
||||||
|
task_cfg = build_task_config(
|
||||||
|
dataset_name, ds_cfg, args.batch_size, enable_thinking, args.seed, limit,
|
||||||
|
args.output_dir, args.model, args.api_url, args.dataset_dir, judge_model_args,
|
||||||
|
run_idx=run_idx,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
run_and_summarize(task_cfg, args.write_summary, args.output_dir, args.model, args.summary_name)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'ERROR in {dataset_name} (run {run_idx + 1 if run_idx else 1}): {e}')
|
||||||
|
|
||||||
|
for dataset_name in multi_run:
|
||||||
|
if dataset_name not in dataset_configs:
|
||||||
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
|
continue
|
||||||
|
num_runs = MULTI_RUN_CONFIG.get(dataset_name, 1)
|
||||||
|
for run_idx in range(num_runs):
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f'Running: {dataset_name} (run {run_idx + 1}/{num_runs}, seed={args.seed})')
|
||||||
|
print(f"{'='*60}")
|
||||||
|
run_one(dataset_name, run_idx=run_idx)
|
||||||
|
|
||||||
|
for dataset_name in single_run:
|
||||||
|
if dataset_name not in dataset_configs:
|
||||||
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
|
continue
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f'Running: {dataset_name} (seed={args.seed})')
|
||||||
|
print(f"{'='*60}")
|
||||||
|
run_one(dataset_name)
|
||||||
|
|
||||||
|
for dataset_name in agent:
|
||||||
|
if dataset_name not in dataset_configs:
|
||||||
|
print(f'WARNING: {dataset_name} not in YAML config, skipping')
|
||||||
|
continue
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f'Running: {dataset_name} (seed={args.seed})')
|
||||||
|
print(f"{'='*60}")
|
||||||
|
run_one(dataset_name)
|
||||||
|
|
||||||
|
if args.write_summary:
|
||||||
|
write_summary(args.output_dir, args.model, args.summary_name)
|
||||||
|
print('\nAll benchmarks done!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
179
bash/test_e2e.py
Normal file
179
bash/test_e2e.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""End-to-end offline test for bash/run.py + collect_results + perf_backup.
|
||||||
|
|
||||||
|
This script does NOT call any model API. It uses an existing benchmark
|
||||||
|
directory (aime24/seed_42) and walks through every code path that the real
|
||||||
|
``run.py`` would touch:
|
||||||
|
|
||||||
|
1. backup_perf_stats — confirm a perf summary snapshot is written
|
||||||
|
2. archive_predictions — confirm per-sample records are deduplicated into
|
||||||
|
the archive
|
||||||
|
3. restore_from_backup — confirm a fresh work_dir can be rehydrated from
|
||||||
|
the backup files
|
||||||
|
4. collect_results aggregation — confirm correct metrics are produced
|
||||||
|
5. Whitelist mode — confirm only the requested benchmarks appear in the
|
||||||
|
summary
|
||||||
|
6. hle_low alias — confirm hle_low directory maps to canonical "hle"
|
||||||
|
7. Reset recovery — confirm that even after wiping the predictions file
|
||||||
|
and the report summary, the archive+backup still recover the metrics
|
||||||
|
|
||||||
|
Run from the project root:
|
||||||
|
|
||||||
|
python bash/test_e2e.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
import collect_results as cr # noqa: E402
|
||||||
|
import perf_backup as pb # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path('/data1/sora/evalscope')
|
||||||
|
SOURCE_OUTPUT = PROJECT_ROOT / 'output'
|
||||||
|
|
||||||
|
|
||||||
|
def banner(msg):
|
||||||
|
print('\n' + '=' * 60)
|
||||||
|
print(f' {msg}')
|
||||||
|
print('=' * 60)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_close(actual, expected, name, tol=1e-3):
|
||||||
|
if abs(actual - expected) > tol:
|
||||||
|
raise AssertionError(f'{name}: expected ~{expected}, got {actual}')
|
||||||
|
print(f' OK {name}={actual:.4f}')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
banner('Setting up isolated test output dir')
|
||||||
|
test_root = PROJECT_ROOT / 'output_e2e_test'
|
||||||
|
if test_root.exists():
|
||||||
|
shutil.rmtree(test_root)
|
||||||
|
test_root.mkdir(parents=True)
|
||||||
|
|
||||||
|
# Copy the canonical aime24 data into the isolated dir
|
||||||
|
src = SOURCE_OUTPUT / 'aime24' / 'seed_42'
|
||||||
|
dst = test_root / 'aime24' / 'seed_42'
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
|
||||||
|
# And copy hle_low as hle_low under the test dir (so we exercise the
|
||||||
|
# alias code path).
|
||||||
|
hle_src = SOURCE_OUTPUT / 'hle_low' / 'seed_42'
|
||||||
|
hle_dst = test_root / 'hle_low' / 'seed_42'
|
||||||
|
if hle_src.exists():
|
||||||
|
shutil.copytree(hle_src, hle_dst)
|
||||||
|
|
||||||
|
benchmark = 'aime24'
|
||||||
|
model_name = 'DeepSeek-V4-Flash-Int8'
|
||||||
|
|
||||||
|
banner('1) backup_perf_stats — snapshot summary')
|
||||||
|
report_json = dst / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
backup_path = pb.backup_perf_stats(test_root, benchmark, model_name, report_json)
|
||||||
|
assert backup_path.exists(), 'backup file was not created'
|
||||||
|
payload = json.loads(backup_path.read_text())
|
||||||
|
assert payload['n_samples'] == 30, f'expected 30 samples, got {payload["n_samples"]}'
|
||||||
|
print(f' OK backup written: {backup_path}, n_samples={payload["n_samples"]}')
|
||||||
|
|
||||||
|
banner('2) archive_predictions — deduplicate into archive')
|
||||||
|
preds = dst / 'predictions' / model_name
|
||||||
|
archive_path = pb.archive_predictions(test_root, benchmark, model_name, preds)
|
||||||
|
assert archive_path.exists(), 'archive file was not created'
|
||||||
|
n_lines = sum(1 for line in archive_path.read_text().splitlines() if line.strip())
|
||||||
|
assert n_lines == 30, f'expected 30 archived samples, got {n_lines}'
|
||||||
|
print(f' OK archive written: {archive_path}, samples={n_lines}')
|
||||||
|
|
||||||
|
banner('3) restore_from_backup — rehydrate a fresh work_dir')
|
||||||
|
fresh_dir = test_root / 'fresh_work' / 'seed_42'
|
||||||
|
fresh_report = fresh_dir / 'reports' / model_name / f'{benchmark}.json'
|
||||||
|
fresh_preds = fresh_dir / 'predictions' / model_name
|
||||||
|
# Pre-create empty structure to mimic what eval_task would do
|
||||||
|
fresh_report.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fresh_report.write_text(json.dumps({
|
||||||
|
'name': f'{model_name}@{benchmark}',
|
||||||
|
'perf_metrics': {'summary': {'n_samples': 1}},
|
||||||
|
}))
|
||||||
|
restored = pb.restore_from_backup(
|
||||||
|
test_root, benchmark, model_name, fresh_preds, fresh_report,
|
||||||
|
)
|
||||||
|
assert restored, 'restore_from_backup returned False'
|
||||||
|
restored_report = json.loads(fresh_report.read_text())
|
||||||
|
assert restored_report['perf_metrics']['summary']['n_samples'] == 30, \
|
||||||
|
f'restored n_samples={restored_report["perf_metrics"]["summary"]["n_samples"]}'
|
||||||
|
print(' OK restored report n_samples=30 (matches backup)')
|
||||||
|
|
||||||
|
banner('4) collect_results — aggregate metrics')
|
||||||
|
csv, xlsx = cr.eval_benchmark([benchmark], test_root, model_name)
|
||||||
|
import pandas as pd
|
||||||
|
df = pd.read_csv(csv)
|
||||||
|
aime24 = df[df['Benchmark'] == 'aime24'].iloc[0]
|
||||||
|
# Single seed_42 run, so values match the report directly (no multi-run
|
||||||
|
# averaging). The full-summary CSV averages across run_1..run_11.
|
||||||
|
assert_close(aime24['得分'], 0.6333, 'aime24 score (single seed)')
|
||||||
|
assert_close(aime24['实测时间(h)'], 0.1469, 'aime24 duration (h)')
|
||||||
|
assert_close(aime24['总样本数'], 30, 'aime24 sample count')
|
||||||
|
assert_close(aime24['延迟_mean(s)'], 35.24971, 'aime24 latency mean')
|
||||||
|
assert_close(aime24['TTFT_mean(s)'], 0.26301, 'aime24 TTFT mean')
|
||||||
|
assert_close(aime24['TPOT_mean(s)'], 0.02165, 'aime24 TPOT mean')
|
||||||
|
assert_close(aime24['输入tokens_mean'], 119.33, 'aime24 input_tokens_mean')
|
||||||
|
assert_close(aime24['输出tokens_mean'], 1609.8, 'aime24 output_tokens_mean')
|
||||||
|
|
||||||
|
banner('5) Whitelist mode — only the requested benchmarks appear')
|
||||||
|
csv2, _ = cr.eval_benchmark([benchmark], test_root, model_name)
|
||||||
|
df2 = pd.read_csv(csv2)
|
||||||
|
assert df2['Benchmark'].dropna().tolist() == ['aime24'], \
|
||||||
|
f'whitelist did not limit benchmarks: {df2["Benchmark"].tolist()}'
|
||||||
|
print(' OK summary only contains aime24 + total')
|
||||||
|
|
||||||
|
banner('6) hle_low → hle alias')
|
||||||
|
if hle_src.exists():
|
||||||
|
csv3, _ = cr.eval_benchmark(['hle'], test_root, model_name)
|
||||||
|
df3 = pd.read_csv(csv3)
|
||||||
|
assert 'hle' in df3['Benchmark'].tolist(), \
|
||||||
|
f'hle canonical name missing from: {df3["Benchmark"].tolist()}'
|
||||||
|
assert 'hle_low' not in df3['Benchmark'].tolist(), \
|
||||||
|
f'hle_low should be aliased away: {df3["Benchmark"].tolist()}'
|
||||||
|
print(' OK hle_low directory maps to canonical "hle"')
|
||||||
|
|
||||||
|
banner('7) Reset recovery — wipe report+predictions, expect archive to restore')
|
||||||
|
# Wipe the current predictions
|
||||||
|
shutil.rmtree(preds)
|
||||||
|
# Reset report.json
|
||||||
|
data = json.loads(report_json.read_text())
|
||||||
|
data['perf_metrics']['summary']['n_samples'] = 1
|
||||||
|
data['perf_metrics']['summary']['latency']['mean'] = 0.0
|
||||||
|
report_json.write_text(json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
csv4, _ = cr.eval_benchmark([benchmark], test_root, model_name)
|
||||||
|
df4 = pd.read_csv(csv4)
|
||||||
|
aime24_after = df4[df4['Benchmark'] == 'aime24'].iloc[0]
|
||||||
|
assert_close(aime24_after['得分'], 0.6333, 'aime24 score (post-reset)')
|
||||||
|
assert_close(aime24_after['总样本数'], 30, 'aime24 sample count (post-reset)')
|
||||||
|
assert_close(aime24_after['延迟_mean(s)'], 35.24971, 'aime24 latency mean (post-reset)')
|
||||||
|
assert_close(aime24_after['TTFT_mean(s)'], 0.26301, 'aime24 TTFT mean (post-reset)')
|
||||||
|
print(' OK archive fully recovered metrics after predictions wipe')
|
||||||
|
|
||||||
|
banner('8) Backup monotonicity — new backup must not overwrite a larger one')
|
||||||
|
# We have a backup with n_samples=30. Now write a fresh report with
|
||||||
|
# n_samples=2 and call backup_perf_stats again — the backup should NOT
|
||||||
|
# be overwritten.
|
||||||
|
data = json.loads(report_json.read_text())
|
||||||
|
data['perf_metrics']['summary']['n_samples'] = 2
|
||||||
|
report_json.write_text(json.dumps(data, indent=2))
|
||||||
|
pb.backup_perf_stats(test_root, benchmark, model_name, report_json)
|
||||||
|
payload2 = json.loads(backup_path.read_text())
|
||||||
|
assert payload2['n_samples'] == 30, \
|
||||||
|
f'backup was clobbered: n_samples now {payload2["n_samples"]}'
|
||||||
|
print(' OK backup preserved n_samples=30 even after a reset run')
|
||||||
|
|
||||||
|
banner('All checks passed')
|
||||||
|
print(f'Test artifacts under {test_root} (kept for inspection)')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -191,3 +191,15 @@ bfcl_v3:
|
|||||||
extra_params:
|
extra_params:
|
||||||
is_fc_model: true
|
is_fc_model: true
|
||||||
underscore_to_dot: true
|
underscore_to_dot: true
|
||||||
|
swe_bench_verified:
|
||||||
|
generation_config:
|
||||||
|
temperature: 0.0
|
||||||
|
top_p: 1.0
|
||||||
|
stream: true
|
||||||
|
max_tokens: 32768
|
||||||
|
swe_bench_pro:
|
||||||
|
generation_config:
|
||||||
|
temperature: 0.0
|
||||||
|
top_p: 1.0
|
||||||
|
stream: true
|
||||||
|
max_tokens: 32768
|
||||||
629
myread.md
Normal file
629
myread.md
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
# EvalScope 评测手册(DeepSeek-V4-Flash-INT8 no-thinking)
|
||||||
|
|
||||||
|
本手册记录基于 EvalScope 对 `DeepSeek-V4-Flash-INT8`(no-thinking 模式)进行能力评测的完整流程,包括代码/数据/镜像下载、环境搭建、benchmark 选择、参数配置、Docker 一键评测和结果收集。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data1/sora/evalscope/
|
||||||
|
├── bash/ # Python 评测入口脚本
|
||||||
|
│ ├── run.py # 唯一主入口
|
||||||
|
│ ├── collect_results.py # 结果汇总成 Excel/CSV
|
||||||
|
│ ├── perf_backup.py # 断点续跑的 perf 持久化备份
|
||||||
|
│ └── pull_swe_bench_images.py # 预拉 SWE-bench 实例镜像
|
||||||
|
├── scripts/ # Shell 辅助脚本
|
||||||
|
│ ├── run_docker_full.sh
|
||||||
|
│ ├── run_docker_lite.sh
|
||||||
|
│ └── run_docker_group.sh
|
||||||
|
├── config/ # 评测参数配置文件
|
||||||
|
│ └── dpv4-int8_nothinking.yaml
|
||||||
|
├── evalscope/ # EvalScope 源码(pip install -e .)
|
||||||
|
├── datasets/ # 评测数据缓存
|
||||||
|
├── output/ # 默认输出目录
|
||||||
|
├── docker/ # Docker 镜像/数据根目录
|
||||||
|
│ ├── images/ # Docker data-root
|
||||||
|
│ └── containerd/ # containerd root
|
||||||
|
├── scritps -> bash/ # 兼容旧路径的软链接
|
||||||
|
└── tools/docker/ # Docker 镜像构建相关
|
||||||
|
```
|
||||||
|
|
||||||
|
### 目录职责
|
||||||
|
|
||||||
|
| 目录 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `bash/` | 可直接执行的 Python 入口脚本。`run.py` 是统一评测入口,`collect_results.py` 用于结果汇总,`perf_backup.py` 维护断点续跑的持久化备份,`pull_swe_bench_images.py` 用于预拉 SWE-bench 镜像。 |
|
||||||
|
| `scripts/` | Shell 辅助脚本,例如宿主机一键启动 Docker 容器。 |
|
||||||
|
| `config/` | YAML 配置文件,集中管理各 benchmark 的 `max_tokens`、`temperature` 等参数。 |
|
||||||
|
| `evalscope/` | EvalScope 源码,通过 `pip install -e .` 安装。 |
|
||||||
|
| `datasets/` | 评测数据缓存目录。 |
|
||||||
|
| `output/` | 默认评测结果输出目录。 |
|
||||||
|
| `docker/images/` | Docker `data-root`,存放所有拉取的镜像层。 |
|
||||||
|
| `docker/containerd/` | containerd `root`,存放镜像元数据。 |
|
||||||
|
|
||||||
|
> **注意**:`scritps` 是历史遗留的目录名(拼写错误),已通过软链接指向 `bash/`。请统一使用 `bash/run.py`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 资源准备
|
||||||
|
|
||||||
|
评测前需要准备四类资源:**代码**、**数据**、**Docker 环境镜像**、**Docker 执行镜像**。下面分别说明下载/加载方式。
|
||||||
|
|
||||||
|
### 2.1 Code(评测入口与配置)
|
||||||
|
|
||||||
|
- 仓库地址:`https://git.meta-stone.net/sora/evalstone.git`
|
||||||
|
- 推荐版本:`798c3d06ab298d7d6b7ed69915d9b01736653bc7`
|
||||||
|
- 本地路径:`/data1/sora/evalscope`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 若已克隆,更新到指定 commit
|
||||||
|
cd /data1/sora/evalscope
|
||||||
|
git fetch origin
|
||||||
|
git checkout 798c3d06ab298d7d6b7ed69915d9b01736653bc7
|
||||||
|
|
||||||
|
# 若首次克隆
|
||||||
|
git clone https://git.meta-stone.net/sora/evalstone.git /data1/sora/evalscope
|
||||||
|
cd /data1/sora/evalscope
|
||||||
|
git checkout 798c3d06ab298d7d6b7ed69915d9b01736653bc7
|
||||||
|
```
|
||||||
|
|
||||||
|
> 该 commit 之后可能有新提交。建议先用 `git log --oneline -5` 查看最新状态;如无特殊说明,默认使用上述版本。
|
||||||
|
|
||||||
|
### 2.2 Data(评测数据集)
|
||||||
|
|
||||||
|
数据集托管在 ModelScope:`SoraAmami/evalscope-datasets`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install modelscope
|
||||||
|
modelscope login --token ms-3d554a39-6e07-496d-8022-0b0ee64a6389
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
from modelscope.hub.snapshot_download import snapshot_download
|
||||||
|
snapshot_download(
|
||||||
|
'SoraAmami/evalscope-datasets',
|
||||||
|
repo_type='dataset',
|
||||||
|
cache_dir='/data1/sora/evalscope',
|
||||||
|
local_dir='/data1/sora/evalscope'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
下载完成后目录结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data1/sora/evalscope/
|
||||||
|
├── datasets/ # benchmark 数据
|
||||||
|
│ ├── AI-ModelScope_gsm8k-*/
|
||||||
|
│ └── ...
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
> **不要把 `dataset_dir` 指向已经包含 `datasets/` 子目录的路径**(例如 `/data1/sora/evalscope/datasets`),否则 evalscope 会到 `datasets/datasets/` 下重新查找/下载,造成数据重复。应指向父目录 `/data1/sora/evalscope`。
|
||||||
|
>
|
||||||
|
> 也不要直接用 `MsDataset.load()` 加载整个数据集,因为不同 benchmark 的 schema 不同,会报 `CastError: column names don't match`。
|
||||||
|
|
||||||
|
### 2.3 Docker 环境镜像(evalscope-complete-py312)
|
||||||
|
|
||||||
|
该镜像包含 Python 3.12 + EvalScope 全部依赖,是主要运行环境。
|
||||||
|
|
||||||
|
**方式一:从本地 tar.gz 加载(离线/已拷贝)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker load -i /data1/sora/evalscope/docker/evalscope-complete-py312.tar.gz
|
||||||
|
docker images | grep evalscope-complete-py312
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:从 ModelScope 下载**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install modelscope
|
||||||
|
modelscope login --token ms-3d554a39-6e07-496d-8022-0b0ee64a6389
|
||||||
|
|
||||||
|
python3 -c "
|
||||||
|
from modelscope.hub.file_download import model_file_download
|
||||||
|
model_file_download(
|
||||||
|
model_id='SoraAmami/evalscope-docker',
|
||||||
|
file_path='evalscope-complete-py312.tar.gz',
|
||||||
|
local_dir='/data1/sora/evalscope/docker'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
|
||||||
|
docker load -i /data1/sora/evalscope/docker/evalscope-complete-py312.tar.gz
|
||||||
|
docker images | grep evalscope-complete-py312
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式三:离线 tar.gz 拷贝**
|
||||||
|
|
||||||
|
如果目标机器无法访问 ModelScope:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在源机器导出
|
||||||
|
# docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
|
||||||
|
|
||||||
|
# 在目标机器加载
|
||||||
|
docker load -i /path/to/docker/evalscope-complete-py312.tar.gz
|
||||||
|
docker images | grep evalscope-complete-py312
|
||||||
|
```
|
||||||
|
|
||||||
|
> 此方式只拷贝 evalscope 运行环境,数据集仍需单独拷贝或下载。
|
||||||
|
|
||||||
|
### 2.4 Docker 执行镜像(代码 sandbox 与 SWE-bench)
|
||||||
|
|
||||||
|
这些镜像用于在容器内执行被测代码,主要包括:
|
||||||
|
|
||||||
|
| 镜像 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `bigcodebench/bigcodebench-evaluate:latest` | `bigcodebench` / `humaneval` 官方 sandbox |
|
||||||
|
| `bigcodebench-sandbox:latest` | 基于官方镜像构建的守护态 sandbox |
|
||||||
|
| `python:3.11-slim` | 通用代码执行环境 |
|
||||||
|
| `swebench/sweb.eval.x86_64.*` | SWE-bench 每个样本一个实例镜像 |
|
||||||
|
|
||||||
|
#### 2.4.1 配置 Docker(国内镜像加速 + 大容量存储路径)
|
||||||
|
|
||||||
|
SWE-bench 镜像数量多、体积大,建议把 Docker `data-root` 和 containerd `root` 都迁移到 `/data1` 大容量盘:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 停止 Docker
|
||||||
|
systemctl stop docker.socket
|
||||||
|
systemctl stop docker
|
||||||
|
systemctl stop containerd
|
||||||
|
|
||||||
|
# 2. 创建新的数据目录
|
||||||
|
mkdir -p /data1/sora/evalscope/docker/images
|
||||||
|
mkdir -p /data1/sora/evalscope/docker/containerd
|
||||||
|
|
||||||
|
# 3. 配置国内镜像加速(多填几个,自动轮询)
|
||||||
|
cat > /etc/docker/daemon.json <<'EOF'
|
||||||
|
{
|
||||||
|
"data-root": "/data1/sora/evalscope/docker/images",
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://docker.m.daocloud.io",
|
||||||
|
"https://docker.1ms.run",
|
||||||
|
"https://docker.1panel.live",
|
||||||
|
"https://hub.rat.dev",
|
||||||
|
"https://hub-mirror.c.163.com",
|
||||||
|
"https://mirror.baidubce.com",
|
||||||
|
"https://dockerproxy.com",
|
||||||
|
"https://docker.mirrors.aliyun.com",
|
||||||
|
"https://docker.mirrors.sjtug.sjtu.edu.cn",
|
||||||
|
"https://docker.nju.edu.cn",
|
||||||
|
"https://docker.mirrors.ustc.edu.cn",
|
||||||
|
"https://docker.mirrors.tuna.tsinghua.edu.cn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. 配置 containerd 数据目录
|
||||||
|
cat > /etc/containerd/config.toml <<'EOF'
|
||||||
|
root = "/data1/sora/evalscope/docker/containerd"
|
||||||
|
state = "/run/containerd"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 5. 重启
|
||||||
|
systemctl reset-failed docker.service
|
||||||
|
systemctl start containerd
|
||||||
|
systemctl start docker
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.2 拉取/构建代码执行镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bigcodebench 官方镜像
|
||||||
|
docker pull bigcodebench/bigcodebench-evaluate:latest
|
||||||
|
|
||||||
|
# 构建守护态 sandbox
|
||||||
|
docker build -t bigcodebench-sandbox:latest -f - . <<'EOF'
|
||||||
|
FROM bigcodebench/bigcodebench-evaluate:latest
|
||||||
|
ENTRYPOINT []
|
||||||
|
CMD ["tail", "-f", "/dev/null"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 通用 Python 执行环境
|
||||||
|
docker pull python:3.11-slim
|
||||||
|
```
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker images | grep -E 'evalscope-complete-py312|bigcodebench|python:3.11-slim'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.3 预拉 SWE-bench 实例镜像(可选,强烈建议)
|
||||||
|
|
||||||
|
SWE-bench 每个样本一个独立 Docker 镜像,评测时再拉会极慢,且容易触发限流。建议提前批量预拉:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data1/sora/evalscope
|
||||||
|
export USE_MODELSCOPE_HUB=1
|
||||||
|
export HF_ENDPOINT=https://hf-mirror.com
|
||||||
|
export PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# 推荐单 worker,避免触发限流
|
||||||
|
python bash/pull_swe_bench_images.py \
|
||||||
|
--dataset swe_bench_verified \
|
||||||
|
--max-workers 1
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本特性:
|
||||||
|
|
||||||
|
- 断点续传:状态文件 `output/.swe_bench_pull_state.json`,中断后再次运行会自动跳过已成功的镜像。
|
||||||
|
- 本地已有镜像自动跳过。
|
||||||
|
- 单 worker 模式稳定,减少限流概率;若网络非常好,可适当提高 `--max-workers`。
|
||||||
|
|
||||||
|
镜像会存到 Docker `data-root` 配置的路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data1/sora/evalscope/docker/images/
|
||||||
|
```
|
||||||
|
|
||||||
|
> 如果拉取过程中频繁失败,可尝试:
|
||||||
|
> 1. 换一个 registry-mirror;
|
||||||
|
> 2. 降低 `--max-workers` 到 1;
|
||||||
|
> 3. 使用 `tmux` 挂后台运行,避免 SSH 断连导致中断。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 环境准备
|
||||||
|
|
||||||
|
### 3.1 方式一:本地 Conda 环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda create -n evalscope python=3.12 -y
|
||||||
|
conda activate evalscope
|
||||||
|
cd /data1/sora/evalscope
|
||||||
|
cd evalscope && pip install -e . && cd ..
|
||||||
|
|
||||||
|
# 关键依赖
|
||||||
|
pip install 'evalscope[terminal_bench,swe_bench,openai_mrcr]'
|
||||||
|
pip install git+https://github.com/sierra-research/tau2-bench@v0.2.0
|
||||||
|
pip install openpyxl # 用于结果汇总
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 方式二:Docker 一键环境(推荐多机复用)
|
||||||
|
|
||||||
|
除 `swe_bench` 系列外,其他 benchmark 均可直接运行。
|
||||||
|
|
||||||
|
**运行容器:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /data1/sora/evalscope/output
|
||||||
|
|
||||||
|
docker run -it --rm \
|
||||||
|
--network host \
|
||||||
|
-v /data1/sora/evalscope:/opt/evalscope \
|
||||||
|
-v /data1/models:/opt/models \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
evalscope-complete-py312:latest \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# 容器内
|
||||||
|
cd /opt/evalscope
|
||||||
|
python bash/run.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
> **数据集路径注意**:evalscope 会在 `--dataset-dir` 后自动拼接 `datasets/` 子目录查找数据。若数据在 `/data1/sora/evalscope/datasets/`,则 `--dataset-dir` 应填 `/data1/sora/evalscope`,不要填 `/data1/sora/evalscope/datasets`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 统一入口:`bash/run.py`
|
||||||
|
|
||||||
|
所有评测都通过这一个脚本启动,全部参数均可通过命令行控制。
|
||||||
|
|
||||||
|
### 4.1 常用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data1/sora/evalscope
|
||||||
|
|
||||||
|
# 1) Full 全量评测(约 75h,CSV 实测完整时间,已含 multi-run)
|
||||||
|
python bash/run.py \
|
||||||
|
--model DeepSeek-V4-Flash-Int8 \
|
||||||
|
--api-url http://localhost:30000/v1 \
|
||||||
|
--dataset-dir /data1/sora/evalscope \
|
||||||
|
--output-dir /data1/sora/evalscope/output \
|
||||||
|
--suite full \
|
||||||
|
--limit none
|
||||||
|
|
||||||
|
# 2) Lite 快速冒烟(约 5h)
|
||||||
|
python bash/run.py --suite lite --limit none
|
||||||
|
|
||||||
|
# 3) Lite 自己选 benchmark
|
||||||
|
python bash/run.py \
|
||||||
|
--datasets humaneval,aime24,gsm8k,arc,longbench_v2,general_fc \
|
||||||
|
--limit 20
|
||||||
|
|
||||||
|
# 4) 只跑某几个 benchmark
|
||||||
|
python bash/run.py --datasets aime24,gsm8k --limit none
|
||||||
|
|
||||||
|
# 5) 多机分组:group1 / group2 / group3
|
||||||
|
python bash/run.py --suite group1 --limit none
|
||||||
|
python bash/run.py --suite group2 --limit none
|
||||||
|
python bash/run.py --suite group3 --limit none
|
||||||
|
|
||||||
|
# 6) 从某个 suite 中排除特定 benchmark
|
||||||
|
python bash/run.py --suite full --exclude hle,trivia_qa --limit none
|
||||||
|
|
||||||
|
# 7) 开启 thinking 模式
|
||||||
|
python bash/run.py --suite lite --thinking
|
||||||
|
|
||||||
|
# 8) 换模型或 API 地址
|
||||||
|
python bash/run.py --model MyModel --api-url http://10.0.0.5:30000/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 参数说明
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `--model` | `DeepSeek-V4-Flash-Int8` | 模型名,会传给 OpenAI API 的 `model` 字段 |
|
||||||
|
| `--api-url` | `http://localhost:30000/v1` | 模型服务地址 |
|
||||||
|
| `--dataset-dir` | `/data1/sora/evalscope` | 数据集父目录,evalscope 会自动找其下的 `datasets/` |
|
||||||
|
| `--output-dir` | `/data1/sora/evalscope/output` | 评测输出根目录 |
|
||||||
|
| `--config` | `config/dpv4-int8_nothinking.yaml` | 各 benchmark 的 `max_tokens` / `temperature` 等配置 |
|
||||||
|
| `--tokenizer-path` | `/data1/models/DeepSeek-V4-Flash-INT8` | 本地 tokenizer 路径,用于长文本 middle-truncation |
|
||||||
|
| `--suite` | `full` | 预置套件:`full` / `lite` / `mid` / `group1` / `group2` / `group3` |
|
||||||
|
| `--datasets` / `--benchmarks` | `None` | 逗号分隔自定义 benchmark 列表,会覆盖 `--suite` |
|
||||||
|
| `--exclude` | `None` | 从当前 suite 中排除的 benchmark |
|
||||||
|
| `--limit` | `None` | 每数据集最多跑几条;`none` / `all` 表示全量 |
|
||||||
|
| `--seed` | `42` | 随机种子 |
|
||||||
|
| `--batch-size` | `4` | 评测并发 |
|
||||||
|
| `--thinking` | `False` | 开启 sglang thinking 模式 |
|
||||||
|
| `--no-thinking` | `False` | 关闭 thinking 模式(默认) |
|
||||||
|
| `--judge-model` | `DeepSeek/DeepSeek-V4-Pro` | 裁判模型名(LLM-as-judge) |
|
||||||
|
| `--judge-api-url` | Vectron 地址 | 裁判模型 API 地址 |
|
||||||
|
| `--judge-api-key` | 内置 key | 裁判模型 API key |
|
||||||
|
| `--judge-max-tokens` | `10240` | 裁判模型最大输出长度 |
|
||||||
|
| `--truncation-tokens` | `131072` | 长文本 middle-truncation token 上限 |
|
||||||
|
| `--no-summary` | `False` | 禁止每次 benchmark 后自动写汇总表 |
|
||||||
|
|
||||||
|
> 汇总表文件名由模型名自动派生,例如 `--model DeepSeek-V4-Flash-Int8` → `<safe_model_name>.csv/.xlsx`。
|
||||||
|
|
||||||
|
### 4.3 套件详情
|
||||||
|
|
||||||
|
| 套件 | 覆盖 benchmark | 预计时间 | 说明 |
|
||||||
|
|------|---------------|----------|------|
|
||||||
|
| `full` | 全部 | 约 **75h** | CSV 实测完整时间,已含 multi-run |
|
||||||
|
| `lite` | 各领域代表作 | 约 **5h** | 快速冒烟,覆盖代码/数学/知识/长文本/Agent |
|
||||||
|
| `mid` | 较全子集 | 约 **25h** | 平衡速度与覆盖 |
|
||||||
|
| `group1` | 代码 + 数学/推理 | 约 **22h** | 多机组 1 |
|
||||||
|
| `group2` | 重知识 | 约 **26h** | 多机组 2 |
|
||||||
|
| `group3` | 长文本 + Agent + 知识 | 约 **27h** | 多机组 3 |
|
||||||
|
|
||||||
|
> **分组时间说明**
|
||||||
|
>
|
||||||
|
> CSV 中记录的是**完整评测时间**(已包含 multi-run),总约 75h。因此分组直接按 CSV 实测时间平衡:Group1 ~22h、Group2 ~26h、Group3 ~27h,合计 ~75h。
|
||||||
|
|
||||||
|
### 4.4 Multi-run 配置
|
||||||
|
|
||||||
|
以下 benchmark 会重复跑多次,再取平均:
|
||||||
|
|
||||||
|
| Benchmark | 次数 | 目的 |
|
||||||
|
|-----------|------|------|
|
||||||
|
| `aime24` / `aime25` / `aime26` / `hmmt26` | 12 | 样本少(各 30 题),多次采样稳定结果 |
|
||||||
|
| `live_code_bench` | 5 | 约 100 样本,多次采样 |
|
||||||
|
| `imo_answerbench` | 4 | 约 120 样本 |
|
||||||
|
| `humaneval` | 3 | 约 164 样本 |
|
||||||
|
| `gpqa_diamond` | 2 | 约 198 样本 |
|
||||||
|
|
||||||
|
multi-run 次数在 `bash/run.py` 的 `MULTI_RUN_CONFIG` 中固定,暂不提供命令行覆盖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 配置文件
|
||||||
|
|
||||||
|
路径:`config/dpv4-int8_nothinking.yaml`
|
||||||
|
|
||||||
|
每个 benchmark 可独立配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
aime24:
|
||||||
|
generation_config:
|
||||||
|
temperature: 1.0 # 多次采样用 1.0
|
||||||
|
top_p: 1.0
|
||||||
|
stream: true
|
||||||
|
max_tokens: 32768
|
||||||
|
|
||||||
|
longbench_v2:
|
||||||
|
generation_config:
|
||||||
|
temperature: 0.0 # 长文本贪心
|
||||||
|
top_p: 1.0
|
||||||
|
stream: true
|
||||||
|
max_tokens: 8192
|
||||||
|
dataset_args:
|
||||||
|
subset_list:
|
||||||
|
- short
|
||||||
|
- medium
|
||||||
|
- long
|
||||||
|
```
|
||||||
|
|
||||||
|
如需换模型,通常只需改 `bash/run.py` 的命令行参数,不需要改 YAML。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 长文本截断
|
||||||
|
|
||||||
|
`longbench_v2` 和 `openai_mrcr` 默认启用 **middle-truncation**,上限由 `--truncation-tokens` 控制(默认 128k)。
|
||||||
|
|
||||||
|
- 对 `longbench_v2`:保留 context 头部和尾部,截断中间。
|
||||||
|
- 对 `openai_mrcr`:保留对话头尾及 needle 附近窗口,并限制单条消息长度。
|
||||||
|
|
||||||
|
如需关闭截断:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bash/run.py --suite full --truncation-tokens 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 结果汇总:`bash/collect_results.py`
|
||||||
|
|
||||||
|
跑完 benchmark 后,自动或手动汇总分数到 Excel/CSV。
|
||||||
|
|
||||||
|
### 7.1 自动汇总(run.py 每次跑完都会更新)
|
||||||
|
|
||||||
|
`run.py` 会在每个 benchmark 结束后自动调用 `collect_results.py`,在按模型名分目录的位置生成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<output-dir>/
|
||||||
|
└── <safe_model_name>/ # 按模型名分目录,目录和文件名均由 --model 自动派生
|
||||||
|
├── <safe_model_name>.xlsx
|
||||||
|
└── <safe_model_name>.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
例:`--model DeepSeek-V4-Flash-Int8` → `/data1/sora/evalscope/output/DeepSeek-V4-Flash-Int8/DeepSeek-V4-Flash-Int8.csv`。
|
||||||
|
|
||||||
|
### 7.2 手动汇总
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 汇总所有已跑完的 benchmark(默认行为)
|
||||||
|
python bash/collect_results.py \
|
||||||
|
--output-dir /data1/sora/evalscope/output \
|
||||||
|
--model DeepSeek-V4-Flash-Int8
|
||||||
|
|
||||||
|
# 只汇总指定 benchmark(推荐,跟 run.py 自动汇总行为一致)
|
||||||
|
python bash/collect_results.py \
|
||||||
|
--output-dir /data1/sora/evalscope/output \
|
||||||
|
--model DeepSeek-V4-Flash-Int8 \
|
||||||
|
--include aime24,gsm8k,arc,longbench_v2
|
||||||
|
|
||||||
|
# 可选:--out-name 自定义文件名(默认就是 safe model name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 汇总规则
|
||||||
|
|
||||||
|
- **只统计本次实际跑的 benchmark**:`run.py` 会维护一个白名单,只把当前这次运行涉及的 benchmark 传给汇总脚本。因此,如果本次只跑了 `aime24,gsm8k,arc`,汇总表里也只会出现这 3 个,不会把 output 目录里以前跑的其它 benchmark 一起拉进来。
|
||||||
|
- 对同一个 benchmark 的多个 seed/run,**分数取平均**。
|
||||||
|
- 性能指标(latency、TTFT、TPOT、TPS 等)**从所有 run 的原始 `predictions/*.jsonl` 重新聚合**,避免断点续跑时从 0 重置。
|
||||||
|
- 输出格式与 `/data1/sora/evalscope/P800模型能力评测结果 - DS4-Flash-INT8-NO-Thinking-2.0-FULL.csv` 保持一致。
|
||||||
|
|
||||||
|
### 7.4 断点续跑统计修复
|
||||||
|
|
||||||
|
**问题**:`evalscope` 在运行时会把所有样本的累计统计写入 `reports/*.json` 里的 `perf_metrics.summary`(旧版本会写到独立的 `perf_stats.json`),并在 `predictions/*.jsonl` 里记录每条样本的原始 perf 数据。如果一次 benchmark 需要跑很久,中间断点续跑时这个累计 summary 会被**重新初始化**(只反映本次重启之后的样本),导致汇总表里 latency / TTFT / TPOT / 总 tokens / 总耗时全部**从 0 开始重新计算**,出现严重低估。
|
||||||
|
|
||||||
|
**修复思路**:保留 `report.json` 里的累计 summary,但**额外维护两份独立于 evalscope 的持久化文件**:
|
||||||
|
|
||||||
|
| 文件 | 内容 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `perf_stats_backup/<benchmark>__<model>.json` | 最近一次完整跑完的 `perf_metrics.summary` | 累计 summary 的稳定副本,避免被重启覆盖 |
|
||||||
|
| `predictions_archive/<benchmark>__<model>.jsonl` | 历次 run 所有样本的原始记录,按 `index` 去重 | 重建 latency / TTFT / TPOT 的真实分布(含 P90/P99) |
|
||||||
|
|
||||||
|
每次 `run_task()` 完成后,`run.py` 会自动调用 `bash/perf_backup.py`:
|
||||||
|
|
||||||
|
1. 把当前 `reports/<model>/<benchmark>.json` 里的 `perf_metrics.summary` 复制到 `perf_stats_backup/<benchmark>__<model>.json`。如果 backup 的 `n_samples` 已经大于新 summary(即检测到 reset),则保留旧 backup,避免覆盖。
|
||||||
|
2. 把当前 `predictions/<model>/*.jsonl` 的每条样本按 `index` 去重追加到 `predictions_archive/<benchmark>__<model>.jsonl`,支持多次断点的样本累积。
|
||||||
|
|
||||||
|
每次下一个 run 开始前,`run.py` 会调用 `restore_from_backup()`:如果 backup 存在,就把 archive 还原回 `predictions/<model>/` 并把 summary 还原回 `report.json` 的 `perf_metrics.summary`,让 evalscope 的 `use_cache` resume 机制能从更大的历史状态继续。
|
||||||
|
|
||||||
|
`collect_results.py` 读取汇总时的优先级:
|
||||||
|
|
||||||
|
1. **优先**读 `predictions_archive/<benchmark>__<model>.jsonl`,按 `index` 去重得到所有历史样本,从原始 perf_metrics 重新计算 latency / TTFT / TPOT 的 mean、P90、P99、QPS、TPS、总 tokens。
|
||||||
|
2. 没有 archive 时,读 `predictions/<model>/*.jsonl`(当前 predictions)。
|
||||||
|
3. 都没有时,回退到 `perf_stats_backup/<benchmark>__<model>.json` 的 summary。
|
||||||
|
4. summary 里 `n_samples` 比 backup 大就用 summary,否则用 backup 里的累计值。
|
||||||
|
|
||||||
|
### 7.5 验证断点续跑确实能恢复
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. 模拟 report.json 被重置(n_samples 改成 2)
|
||||||
|
# 2. 不动 predictions 和 archive
|
||||||
|
# 3. 跑 collect_results
|
||||||
|
# 期望:汇总表仍显示 30 个样本的真实指标
|
||||||
|
```
|
||||||
|
|
||||||
|
实际验证结果:把 `aime24/report.json` 的 `n_samples` 改成 2 后再汇总,得分仍是 **0.6167**、耗时 **1.22h**、**TTFT/TPOT 正常值**,证明 archive 修复确实生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 输出目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
<output-dir>/
|
||||||
|
├── <benchmark_name>/
|
||||||
|
│ ├── seed_<seed>/
|
||||||
|
│ │ ├── reports/<model_name>/<benchmark_name>.json
|
||||||
|
│ │ ├── predictions/<model_name>/<benchmark_name>.jsonl
|
||||||
|
│ │ └── logs/eval_log.log
|
||||||
|
│ └── seed_<seed>_run_<idx>/ # multi-run 额外目录
|
||||||
|
├── perf_stats_backup/ # 累计 summary 持久化(断点恢复)
|
||||||
|
│ └── <benchmark>__<model>.json
|
||||||
|
├── predictions_archive/ # 原始每条样本归档(断点恢复)
|
||||||
|
│ └── <benchmark>__<model>.jsonl
|
||||||
|
└── <safe_model_name>/ # 汇总表(按模型名)
|
||||||
|
├── <safe_model_name>.xlsx
|
||||||
|
└── <safe_model_name>.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看分数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 单个 benchmark
|
||||||
|
cat output/aime24/seed_42/reports/DeepSeek-V4-Flash-Int8/aime24.json
|
||||||
|
|
||||||
|
# 汇总所有分数
|
||||||
|
python3 -c "
|
||||||
|
import json, glob
|
||||||
|
for f in sorted(glob.glob('output/*/seed_*/reports/*/*.json')):
|
||||||
|
data = json.load(open(f))
|
||||||
|
score = data.get('score', data.get('mean_acc', 'N/A'))
|
||||||
|
print(f'{f}: {score}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 最终 Benchmark 集合
|
||||||
|
|
||||||
|
| 能力域 | Benchmark |
|
||||||
|
|--------|-----------|
|
||||||
|
| 代码生成 | `humaneval`, `live_code_bench`, `bigcodebench` |
|
||||||
|
| 推理/数学 | `aime24`, `aime25`, `aime26`, `hmmt26`, `imo_answerbench`, `gsm8k`, `competition_math`, `bbh`, `drop` |
|
||||||
|
| 知识 | `gpqa_diamond`, `hle`, `mmlu_pro`, `simple_qa`, `super_gpqa`, `mmlu`, `cmmlu`, `arc`, `hellaswag`, `trivia_qa`, `winogrande` |
|
||||||
|
| 长上下文 | `longbench_v2`, `openai_mrcr` |
|
||||||
|
| 智能体/工具 | `tau2_bench`, `general_fc`, `bfcl_v3` |
|
||||||
|
|
||||||
|
未纳入标准流程的:
|
||||||
|
|
||||||
|
- `swe_bench_pro` / `swe_bench_verified` / `swe_bench_multilingual_agentic`:每个样本需独立 Docker 镜像,建议按 2.4.3 预拉后单独运行。
|
||||||
|
- `browsecomp` / `mcp_atlas`:需要外部 MCP 搜索工具。
|
||||||
|
- `terminal_bench_v2`:镜像兼容性问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
### Q1: Docker 里找不到 datasets?
|
||||||
|
|
||||||
|
确保 `--dataset-dir` 指向包含 `datasets/` 的**父目录**,而不是 `datasets/` 本身。
|
||||||
|
|
||||||
|
### Q2: `bigcodebench` 或 `humaneval` 报 sandbox 错误?
|
||||||
|
|
||||||
|
检查是否已准备镜像:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker images | grep -E 'bigcodebench-sandbox|python:3.11-slim'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q3: 只想跑某个 benchmark 怎么办?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bash/run.py --datasets aime24 --limit none
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4: 想换 judge model?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python bash/run.py \
|
||||||
|
--judge-model deepseek-v4-pro \
|
||||||
|
--judge-api-url https://api.deepseek.com/v1 \
|
||||||
|
--judge-api-key sk-xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5: 断点续跑后 perf 指标被重置?
|
||||||
|
|
||||||
|
`run.py` 会在每次 benchmark 后把 `perf_metrics.summary` 和 `predictions` 备份到 `output/perf_stats_backup/` 和 `output/predictions_archive/`;汇总时优先从 archive 重新计算,因此断点重启不会导致指标被低估。同时汇总表只包含**本次运行实际跑的 benchmark**。
|
||||||
|
|
||||||
|
### Q6: SWE-bench 镜像拉取慢/被限流?
|
||||||
|
|
||||||
|
1. 按 2.4.1 配置多 mirror;
|
||||||
|
2. 使用 `--max-workers 1` 单 worker 拉取;
|
||||||
|
3. 用 `tmux` 或 `nohup` 挂后台,避免 SSH 断连;
|
||||||
|
4. 若某个 mirror 完全不可用,从 `daemon.json` 中移除,避免无效重试。
|
||||||
Loading…
x
Reference in New Issue
Block a user