fusion-llm-demo / evaluation /benchmark_runner.py
zhan1206
fix: comprehensive audit fixes (ONNX/export, benchmark, deprecated APIs, attention unification)
f39f340
Raw
History Blame
25.3 kB
"""
基准测试运行器
支持多种评估任务和配置,包括标准学术 benchmark
"""
import sys
import json
import time
import torch
import argparse
from pathlib import Path
from typing import Dict, List, Optional, Any
from datetime import datetime
sys.path.insert(0, '.')
from evaluation.metrics import ModelEvaluator, EvaluationMetrics
from evaluation.bertscore_moverscore import bertscore_simple, moverscore_simple
class BenchmarkRunner:
"""
基准测试运行器
支持的测试类型:
- perplexity: 困惑度评估
- generation: 生成质量评估(BERTScore/MoverScore)
- mmlu: MMLU 多任务语言理解
- gsm8k: GSM8K 数学推理
- speed: 推理速度基准
"""
def __init__(self, model_path: str, device: str = "auto", tokenizer_path: Optional[str] = None):
"""
初始化基准测试运行器
Args:
model_path: 模型路径
device: 计算设备 (auto/cpu/cuda)
tokenizer_path: tokenizer 路径(如有)
"""
self.model_path = model_path
self.device = self._resolve_device(device)
self.model = None
self.config = None
self.tokenizer = None
self.tokenizer_path = tokenizer_path
def _resolve_device(self, device: str) -> str:
"""解析设备字符串"""
if device == "auto":
return "cuda" if torch.cuda.is_available() else "cpu"
return device
def load_model(self):
"""加载模型和 tokenizer"""
print(f"[Loading] Model from {self.model_path} on {self.device}")
# 尝试加载 FusionMini
try:
from models.fusion_mini import FusionMini, FusionMiniConfig
self.model = FusionMini._load_from_safetensors(self.model_path)
self.config = self.model.config
print(f"[Loaded] FusionMini model")
except Exception as e:
# 回退到 FusionModel
from models.fusion_model import FusionModel, FusionConfig
self.model = FusionModel.from_pretrained(self.model_path)
self.config = self.model.config
print(f"[Loaded] FusionModel: {e}")
self.model.to(self.device)
self.model.eval()
# 加载 tokenizer
self._load_tokenizer()
def _load_tokenizer(self):
"""加载 tokenizer(优先从路径加载,否则用简单 tokenizer)"""
# 尝试加载真实 tokenizer
if self.tokenizer_path and Path(self.tokenizer_path).exists():
try:
from transformers import AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.tokenizer_path)
print(f"[Tokenizer] Loaded from {self.tokenizer_path}")
return
except Exception as e:
print(f"[Tokenizer] Failed to load from {self.tokenizer_path}: {e}")
# 回退:尝试从模型路径加载
try:
from transformers import AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
print(f"[Tokenizer] Loaded from model path {self.model_path}")
return
except Exception:
pass
# 最终回退:简单字符级 tokenizer
print("[Tokenizer] Using simple character-level tokenizer")
self.tokenizer = self._create_simple_tokenizer()
def _create_simple_tokenizer(self):
"""创建简单 tokenizer(用于测试)"""
vocab_size = getattr(self.config, 'vocab_size', 10000)
class SimpleTokenizer:
def __init__(self, vs):
self.vocab_size = vs
def encode(self, text):
return [ord(c) % self.vocab_size for c in text[:512]]
def decode(self, ids):
return ''.join(chr(max(32, i % 128)) for i in ids if 0 <= i < self.vocab_size)
return SimpleTokenizer(vocab_size)
# ==================== Perplexity ====================
def run_perplexity(self, texts: List[str]) -> Dict[str, float]:
"""
计算困惑度
Args:
texts: 测试文本列表
Returns:
困惑度指标
"""
print(f"\n[Benchmark] Perplexity on {len(texts)} texts")
self.model.eval()
total_loss = 0.0
total_tokens = 0
with torch.no_grad():
for text in texts:
ids = self.tokenizer.encode(text)
if hasattr(ids, 'input_ids'):
ids = ids.input_ids
if len(ids) < 2:
continue
input_ids = torch.tensor([ids], device=self.device)
labels = input_ids.clone()
outputs = self.model(input_ids=input_ids, labels=labels)
loss = outputs.loss
if loss is not None:
total_loss += loss.item() * len(ids)
total_tokens += len(ids)
if total_tokens == 0:
return {"perplexity": float('inf')}
avg_loss = total_loss / total_tokens
perplexity = torch.exp(torch.tensor(avg_loss)).item()
return {
"perplexity": perplexity,
"avg_loss": avg_loss,
"total_tokens": total_tokens
}
# ==================== Generation Quality ====================
def run_generation_quality(
self,
prompts: List[str],
references: List[str],
max_new_tokens: int = 50
) -> Dict[str, Any]:
"""
生成质量评估(BERTScore + MoverScore)
Args:
prompts: 提示列表
references: 参考答案列表
max_new_tokens: 最大生成 token 数
Returns:
生成质量指标
"""
print(f"\n[Benchmark] Generation quality on {len(prompts)} prompts")
generations = []
self.model.eval()
with torch.no_grad():
for prompt in prompts:
encoded = self.tokenizer.encode(prompt)
if hasattr(encoded, 'input_ids'):
input_ids = torch.tensor(encoded.input_ids, device=self.device).unsqueeze(0)
else:
input_ids = torch.tensor([encoded], device=self.device)
# 简单贪婪生成
generated = input_ids.clone()
for _ in range(max_new_tokens):
outputs = self.model(input_ids=generated)
next_token = outputs.logits[:, -1, :].argmax(dim=-1, keepdim=True)
generated = torch.cat([generated, next_token], dim=1)
# EOS 检查
eos_token_id = getattr(self.tokenizer, 'eos_token_id', 2)
if next_token.item() == eos_token_id:
break
gen_text = self.tokenizer.decode(generated[0].tolist())
generations.append(gen_text)
# 计算 BERTScore 和 MoverScore
bert_scores = []
mover_scores = []
for gen, ref in zip(generations, references):
gen_ids = self.tokenizer.encode(gen)
ref_ids = self.tokenizer.encode(ref)
if hasattr(gen_ids, 'input_ids'):
gen_ids = gen_ids.input_ids
if hasattr(ref_ids, 'input_ids'):
ref_ids = ref_ids.input_ids
_, _, bert_f1 = bertscore_simple(gen_ids, ref_ids)
mover_score = moverscore_simple(gen_ids, ref_ids)
bert_scores.append(bert_f1)
mover_scores.append(mover_score)
return {
"bertscore_f1": sum(bert_scores) / len(bert_scores) if bert_scores else 0.0,
"moverscore": sum(mover_scores) / len(mover_scores) if mover_scores else 0.0,
"generations": generations[:5],
"num_samples": len(generations)
}
# ==================== MMLU ====================
def run_mmlu(self, num_samples: int = 100, few_shot: int = 5) -> Dict[str, Any]:
"""
运行 MMLU 基准测试
MMLU (Massive Multitask Language Understanding) 测试多任务语言理解能力。
Args:
num_samples: 每个子集采样数量(<=0 表示全部)
few_shot: few-shot 示例数量
Returns:
MMLU 准确率等指标
"""
print(f"\n[Benchmark] MMLU (few_shot={few_shot}, samples={num_samples})")
try:
from datasets import load_dataset
except ImportError:
print("[MMLU] datasets package not installed. pip install datasets")
return {"error": "datasets not installed"}
dataset = load_dataset("cais/mmlu", "all", split="test")
if num_samples > 0:
dataset = dataset.shuffle(seed=42).select(range(min(num_samples, len(dataset))))
correct = 0
total = 0
results_by_subject = {}
self.model.eval()
with torch.no_grad():
for sample in dataset:
subject = sample["subject"]
question = sample["question"]
choices = sample["choices"]
correct_answer = sample["answer"]
# 构建 prompt
prompt = self._build_mmlu_prompt(question, choices, few_shot)
# 生成答案
encoded = self.tokenizer.encode(prompt)
if hasattr(encoded, 'input_ids'):
input_ids = torch.tensor(encoded.input_ids, device=self.device).unsqueeze(0)
else:
input_ids = torch.tensor([encoded], device=self.device)
outputs = self.model(input_ids=input_ids)
logits = outputs.logits[:, -1, :]
# 解析答案(A/B/C/D)
pred = self._parse_mmlu_answer(logits, prompt)
is_correct = (pred == correct_answer)
correct += int(is_correct)
total += 1
if subject not in results_by_subject:
results_by_subject[subject] = {"correct": 0, "total": 0}
results_by_subject[subject]["correct"] += int(is_correct)
results_by_subject[subject]["total"] += 1
overall_acc = correct / total if total > 0 else 0.0
# 按学科汇总
subject_accs = {}
for subj, stats in results_by_subject.items():
subject_accs[subj] = stats["correct"] / stats["total"] if stats["total"] > 0 else 0.0
return {
"mmlu_overall_accuracy": overall_acc,
"mmlu_total": total,
"mmlu_subject_accuracy": subject_accs,
"mmlu_num_subjects": len(subject_accs)
}
def _build_mmlu_prompt(self, question: str, choices: List[str], few_shot: int) -> str:
"""构建 MMLU few-shot prompt"""
# 简化版:直接拼接问题和选项
letters = ["A", "B", "C", "D"]
choice_text = "\n".join(f"{letters[i]}. {c}" for i, c in enumerate(choices[:4]))
prompt = f"Question: {question}\n{choice_text}\nAnswer:"
return prompt
def _parse_mmlu_answer(self, logits, prompt: str) -> int:
"""解析 MMLU 答案(A/B/C/D)"""
# 简化版:检查 logits 中 A/B/C/D token 的概率
# 实际应 tokenizer 编码 "A"/"B"/"C"/"D" 后取对应 logits
answer_tokens = []
for letter in ["A", "B", "C", "D"]:
encoded = self.tokenizer.encode(letter)
if hasattr(encoded, 'input_ids'):
answer_tokens.append(encoded.input_ids[0])
else:
answer_tokens.append(encoded[0] if encoded else 0)
# 取最后一个 token 的 logits(简化)
last_logits = logits[0]
answer_logits = [last_logits[t].item() for t in answer_tokens]
return int(torch.tensor(answer_logits).argmax().item())
# ==================== GSM8K ====================
def run_gsm8k(self, num_samples: int = 100) -> Dict[str, Any]:
"""
运行 GSM8K 基准测试
GSM8K (Grade School Math 8K) 测试小学数学推理能力。
Args:
num_samples: 采样数量(<=0 表示全部)
Returns:
GSM8K 准确率等指标
"""
print(f"\n[Benchmark] GSM8K (samples={num_samples})")
try:
from datasets import load_dataset
except ImportError:
print("[GSM8K] datasets package not installed. pip install datasets")
return {"error": "datasets not installed"}
dataset = load_dataset("gsm8k", "main", split="test")
if num_samples > 0:
dataset = dataset.shuffle(seed=42).select(range(min(num_samples, len(dataset))))
correct = 0
total = 0
gen_examples = []
self.model.eval()
with torch.no_grad():
for sample in dataset:
question = sample["question"]
answer_text = sample["answer"]
# 提取正确答案(GSM8K 答案格式:#### 数字)
correct_answer = self._extract_gsm8k_answer(answer_text)
# 生成
prompt = f"Question: {question}\nAnswer:"
encoded = self.tokenizer.encode(prompt)
if hasattr(encoded, 'input_ids'):
input_ids = torch.tensor(encoded.input_ids, device=self.device).unsqueeze(0)
else:
input_ids = torch.tensor([encoded], device=self.device)
# 贪婪生成
generated = input_ids.clone()
for _ in range(128):
outputs = self.model(input_ids=generated)
next_token = outputs.logits[:, -1, :].argmax(dim=-1, keepdim=True)
generated = torch.cat([generated, next_token], dim=1)
eos_token_id = getattr(self.tokenizer, 'eos_token_id', 2)
if next_token.item() == eos_token_id:
break
gen_text = self.tokenizer.decode(generated[0].tolist())
pred_answer = self._extract_gsm8k_answer(gen_text)
is_correct = (pred_answer == correct_answer)
correct += int(is_correct)
total += 1
if len(gen_examples) < 3:
gen_examples.append({
"question": question[:100],
"pred": pred_answer,
"gold": correct_answer,
"correct": is_correct
})
accuracy = correct / total if total > 0 else 0.0
return {
"gsm8k_accuracy": accuracy,
"gsm8k_total": total,
"gsm8k_correct": correct,
"gsm8k_examples": gen_examples
}
def _extract_gsm8k_answer(self, text: str) -> Optional[float]:
"""从 GSM8K 文本中提取数值答案"""
import re
# GSM8K 格式:#### 42
match = re.search(r'####\s*(-?\d+(?:\.\d+)?)', text)
if match:
try:
return float(match.group(1))
except ValueError:
pass
# 回退:取最后一个数字
numbers = re.findall(r'-?\d+(?:\.\d+)?', text)
if numbers:
try:
return float(numbers[-1])
except ValueError:
pass
return None
# ==================== HumanEval (Placeholder) ====================
def run_humaneval(self, num_samples: int = 50) -> Dict[str, Any]:
"""
运行 HumanEval 基准测试(占位实现)
HumanEval 需要代码执行环境,此处提供框架,
完整实现需要 integrate 代码执行沙箱。
Args:
num_samples: 采样数量
Returns:
占位结果
"""
print(f"\n[Benchmark] HumanEval (samples={num_samples}) [PLACEHOLDER]")
print("[HumanEval] Note: Full implementation requires code execution sandbox.")
print("[HumanEval] Install: pip install human-eval")
try:
from datasets import load_dataset
except ImportError:
print("[HumanEval] datasets package not installed. pip install datasets")
return {"error": "datasets not installed", "note": "placeholder"}
dataset = load_dataset("openai_humaneval", split="test")
if num_samples > 0:
dataset = dataset.shuffle(seed=42).select(range(min(num_samples, len(dataset))))
# 占位:记录 prompt,但不执行代码
results = []
for sample in dataset:
results.append({
"task_id": sample["task_id"],
"prompt": sample["prompt"][:100],
"note": "Code execution not implemented; install human-eval package for full evaluation"
})
return {
"humaneval_samples": len(results),
"humaneval_note": "Placeholder. Install `human-eval` and implement code execution for full evaluation.",
"humaneval_samples_preview": results[:3]
}
# ==================== Speed Benchmark ====================
def run_speed_benchmark(
self,
batch_sizes: List[int] = [1, 2, 4],
seq_lengths: List[int] = [32, 64, 128, 256],
warmup: int = 3,
runs: int = 10
) -> Dict[str, Any]:
"""
推理速度基准测试
Args:
batch_sizes: 批大小列表
seq_lengths: 序列长度列表
warmup: 预热次数
runs: 测试次数
Returns:
速度指标
"""
print(f"\n[Benchmark] Speed benchmark")
results = []
vocab_size = getattr(self.config, 'vocab_size', 10000)
self.model.eval()
for batch_size in batch_sizes:
for seq_len in seq_lengths:
# 预热
for _ in range(warmup):
dummy = torch.randint(0, vocab_size, (batch_size, seq_len), device=self.device)
with torch.no_grad():
_ = self.model(input_ids=dummy)
# 计时
if self.device == "cuda":
torch.cuda.synchronize()
start = time.perf_counter()
for _ in range(runs):
dummy = torch.randint(0, vocab_size, (batch_size, seq_len), device=self.device)
with torch.no_grad():
_ = self.model(input_ids=dummy)
if self.device == "cuda":
torch.cuda.synchronize()
end = time.perf_counter()
avg_time = (end - start) / runs
throughput = batch_size * seq_len / avg_time
results.append({
"batch_size": batch_size,
"seq_len": seq_len,
"latency_ms": avg_time * 1000,
"throughput_tokens_per_sec": throughput
})
print(f" batch={batch_size}, seq={seq_len}: {avg_time*1000:.2f}ms")
return {
"results": results,
"device": self.device,
"runs": runs
}
# ==================== Full Benchmark ====================
def run_full_benchmark(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
运行完整基准测试
Args:
config: 测试配置
Returns:
完整测试结果
"""
print("="*60)
print("Fusion-LLM Benchmark Runner")
print("="*60)
self.load_model()
results = {
"model_path": self.model_path,
"device": self.device,
"timestamp": datetime.now().isoformat(),
"config": self.config.to_dict() if hasattr(self.config, 'to_dict') else {}
}
# 困惑度
if config.get("perplexity", True):
test_texts = [
"The quick brown fox jumps over the lazy dog.",
"Machine learning models require large amounts of data.",
"Natural language processing enables computers to understand text."
]
results["perplexity"] = self.run_perplexity(test_texts)
# 生成质量
if config.get("generation", True):
prompts = ["The future of AI is", "Machine learning helps"]
references = ["The future of AI is bright and transformative.", "Machine learning helps solve complex problems."]
results["generation"] = self.run_generation_quality(prompts, references)
# MMLU
if config.get("mmlu", False):
results["mmlu"] = self.run_mmlu(
num_samples=config.get("mmlu_samples", 100),
few_shot=config.get("mmlu_few_shot", 5)
)
# GSM8K
if config.get("gsm8k", False):
results["gsm8k"] = self.run_gsm8k(
num_samples=config.get("gsm8k_samples", 100)
)
# HumanEval
if config.get("humaneval", False):
results["humaneval"] = self.run_humaneval(
num_samples=config.get("humaneval_samples", 50)
)
# 速度基准
if config.get("speed", False):
results["speed"] = self.run_speed_benchmark(
batch_sizes=config.get("batch_sizes", [1]),
seq_lengths=config.get("seq_lengths", [32, 64, 128])
)
print("\n[Benchmark] Complete")
return results
def main():
parser = argparse.ArgumentParser(description="Fusion-LLM Benchmark Runner")
parser.add_argument("--model", required=True, help="Path to model checkpoint")
parser.add_argument("--tokenizer", default=None, help="Tokenizer path (optional)")
parser.add_argument("--device", default="auto", help="Device (auto/cpu/cuda)")
parser.add_argument("--output", default="benchmark_results.json", help="Output file")
# 测试项目开关
parser.add_argument("--perplexity", action="store_true", help="Run perplexity benchmark")
parser.add_argument("--generation", action="store_true", help="Run generation benchmark")
parser.add_argument("--mmlu", action="store_true", help="Run MMLU benchmark")
parser.add_argument("--gsm8k", action="store_true", help="Run GSM8K benchmark")
parser.add_argument("--humaneval", action="store_true", help="Run HumanEval benchmark (placeholder)")
parser.add_argument("--speed", action="store_true", help="Run speed benchmark")
parser.add_argument("--all", action="store_true", help="Run all benchmarks")
# MMLU/GSM8K 参数
parser.add_argument("--mmlu-samples", type=int, default=100, help="MMLU samples per subject")
parser.add_argument("--mmlu-few-shot", type=int, default=5, help="MMLU few-shot count")
parser.add_argument("--gsm8k-samples", type=int, default=100, help="GSM8K samples")
parser.add_argument("--humaneval-samples", type=int, default=50, help="HumanEval samples")
args = parser.parse_args()
# 配置
config = {
"perplexity": args.perplexity or args.all,
"generation": args.generation or args.all,
"mmlu": args.mmlu or args.all,
"gsm8k": args.gsm8k or args.all,
"humaneval": args.humaneval or args.all,
"speed": args.speed or args.all,
"mmlu_samples": args.mmlu_samples,
"mmlu_few_shot": args.mmlu_few_shot,
"gsm8k_samples": args.gsm8k_samples,
"humaneval_samples": args.humaneval_samples,
}
# 运行
runner = BenchmarkRunner(args.model, args.device, args.tokenizer)
results = runner.run_full_benchmark(config)
# 保存
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False, default=str)
print(f"\n[Saved] Results to {args.output}")
if __name__ == "__main__":
main()