| """ |
| Run official LLM benchmarks with FluxEM + Qwen3 tool-calling. |
| |
| Supports GSM8K and Hendrycks MATH datasets via Hugging Face datasets. |
| """ |
|
|
| import argparse |
| import json |
| import re |
| import sys |
| import time |
| from pathlib import Path |
| from typing import Any, Dict, Optional, Tuple |
|
|
| from datasets import load_dataset, concatenate_datasets, get_dataset_config_names |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
| sys.path.insert(1, str(Path(__file__).parent.parent.parent)) |
|
|
| from .qwen3_wrapper import create_wrapper, Qwen3MLXWrapper |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser( |
| description="Run official benchmarks with FluxEM tool-calling.", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| ) |
| parser.add_argument( |
| "--dataset", |
| choices=["gsm8k", "math"], |
| default="gsm8k", |
| help="Official dataset to run (default: gsm8k).", |
| ) |
| parser.add_argument( |
| "--split", |
| type=str, |
| default="test", |
| help="Dataset split to evaluate (default: test).", |
| ) |
| parser.add_argument( |
| "--limit", |
| type=int, |
| default=0, |
| help="Limit the number of samples (0 = full split).", |
| ) |
| parser.add_argument( |
| "--model-path", |
| type=str, |
| default="~/.mlx/models/Qwen/Qwen3-4B-Instruct-MLX", |
| help="Path to Qwen3-4B MLX model", |
| ) |
| parser.add_argument( |
| "--transformers-model-path", |
| type=str, |
| default=None, |
| help="Transformers model ID or local path (fallback when MLX unavailable).", |
| ) |
| parser.add_argument( |
| "--transformers-device", |
| type=str, |
| default="cpu", |
| help="Device for transformers backend (cpu/cuda/mps).", |
| ) |
| parser.add_argument( |
| "--transformers-trust-remote-code", |
| action="store_true", |
| help="Allow trust_remote_code for transformers loading.", |
| ) |
| parser.add_argument( |
| "--allow-model-download", |
| action="store_true", |
| help="Allow transformers to download model files if missing locally.", |
| ) |
| parser.add_argument( |
| "--max-tokens", |
| type=int, |
| default=2048, |
| help="Max tokens for baseline generation (default: 2048).", |
| ) |
| parser.add_argument( |
| "--tool-selection", |
| choices=["pattern", "llm", "hybrid"], |
| default="llm", |
| help="Tool selection strategy (default: llm).", |
| ) |
| parser.add_argument( |
| "--llm-query-extraction", |
| action="store_true", |
| help="Use LLM to refine tool queries when selecting tools via LLM.", |
| ) |
| parser.add_argument( |
| "--no-baseline", |
| action="store_true", |
| help="Skip baseline evaluation (LLM without tools).", |
| ) |
| parser.add_argument( |
| "--output", |
| type=str, |
| default="experiments/qwen3_toolcalling/results", |
| help="Output directory for results", |
| ) |
| parser.add_argument( |
| "--verbose", |
| action="store_true", |
| help="Print detailed progress", |
| ) |
| return parser.parse_args() |
|
|
|
|
| def _extract_last_number(text: str) -> Optional[float]: |
| if not text: |
| return None |
| cleaned = text.replace(",", "") |
| matches = re.findall(r"-?\d+\.?\d*", cleaned) |
| if not matches: |
| return None |
| token = matches[-1] |
| try: |
| if "." in token: |
| return float(token) |
| return float(int(token)) |
| except ValueError: |
| return None |
|
|
|
|
| def _parse_fraction(token: str) -> Optional[float]: |
| if re.match(r"^-?\d+/\d+$", token): |
| num, den = token.split("/", 1) |
| try: |
| return float(num) / float(den) |
| except (ValueError, ZeroDivisionError): |
| return None |
| frac_match = re.match(r"^\\frac\{(-?\d+)\}\{(\d+)\}$", token) |
| if frac_match: |
| num, den = frac_match.group(1), frac_match.group(2) |
| try: |
| return float(num) / float(den) |
| except (ValueError, ZeroDivisionError): |
| return None |
| return None |
|
|
|
|
| def _normalize_text(text: str) -> str: |
| return re.sub(r"\s+", "", text or "").strip() |
|
|
|
|
| def _extract_gsm8k_answer(answer: str) -> Optional[float]: |
| if not answer: |
| return None |
| if "####" in answer: |
| answer = answer.split("####")[-1].strip() |
| frac_val = _parse_fraction(answer) |
| if frac_val is not None: |
| return frac_val |
| return _extract_last_number(answer) |
|
|
|
|
| def _extract_math_answer(sample: Dict[str, Any]) -> str: |
| if "answer" in sample and sample["answer"]: |
| return str(sample["answer"]).strip() |
| solution = sample.get("solution", "") |
| match = re.search(r"\\boxed\{([^}]*)\}", solution) |
| if match: |
| return match.group(1).strip() |
| return solution.strip() |
|
|
|
|
| def _extract_prediction(response: Dict[str, Any]) -> Tuple[Optional[float], str]: |
| if response.get("tool_success"): |
| result = response.get("result") |
| if isinstance(result, (int, float)): |
| return float(result), str(result) |
| if isinstance(result, (list, tuple)): |
| return None, str(result) |
| if isinstance(result, str): |
| num = _extract_last_number(result) |
| return num, result |
| return None, str(result) |
| text = response.get("response", "") |
| return _extract_last_number(text), text |
|
|
|
|
| def _compare_numeric(expected: Optional[float], predicted: Optional[float]) -> bool: |
| if expected is None or predicted is None: |
| return False |
| return abs(expected - predicted) < 1e-6 |
|
|
|
|
| def load_official_dataset(dataset_name: str, split: str): |
| if dataset_name == "gsm8k": |
| return load_dataset("gsm8k", "main", split=split) |
| if dataset_name == "math": |
| last_error = None |
| try: |
| configs = get_dataset_config_names("EleutherAI/hendrycks_math") |
| datasets = [ |
| load_dataset("EleutherAI/hendrycks_math", config, split=split) |
| for config in configs |
| ] |
| return concatenate_datasets(datasets) |
| except Exception as exc: |
| last_error = exc |
| candidates = [ |
| ("hendrycks/math", "all"), |
| ("hendrycks/math", None), |
| ] |
| for dataset_id, config in candidates: |
| try: |
| if config: |
| return load_dataset(dataset_id, config, split=split) |
| return load_dataset(dataset_id, split=split) |
| except Exception as exc: |
| last_error = exc |
| continue |
| raise RuntimeError(f"Failed to load MATH dataset: {last_error}") from last_error |
| raise ValueError(f"Unsupported dataset: {dataset_name}") |
|
|
|
|
| def run_benchmark( |
| wrapper: Qwen3MLXWrapper, |
| dataset_name: str, |
| split: str, |
| limit: int, |
| run_baseline: bool, |
| verbose: bool, |
| ) -> Dict[str, Any]: |
| dataset = load_official_dataset(dataset_name, split) |
| if limit and limit > 0: |
| dataset = dataset.select(range(min(limit, len(dataset)))) |
|
|
| results = [] |
| correct_tool = 0 |
| correct_baseline = 0 |
| tool_success_count = 0 |
|
|
| for idx, sample in enumerate(dataset): |
| if dataset_name == "gsm8k": |
| prompt = sample["question"] |
| expected_value = _extract_gsm8k_answer(sample["answer"]) |
| expected_text = str(expected_value) if expected_value is not None else "" |
| else: |
| prompt = sample["problem"] |
| expected_text = _extract_math_answer(sample) |
| expected_value = _extract_last_number(expected_text) |
|
|
| if verbose: |
| print(f"\n[{idx + 1}/{len(dataset)}] {prompt[:80]}...") |
|
|
| tool_response = wrapper.generate_with_tools(prompt) |
| tool_num, tool_text = _extract_prediction(tool_response) |
| tool_success = bool(tool_response.get("tool_success")) |
| tool_success_count += 1 if tool_success else 0 |
|
|
| tool_correct = False |
| if dataset_name == "gsm8k": |
| tool_correct = _compare_numeric(expected_value, tool_num) |
| else: |
| tool_correct = _normalize_text(expected_text) == _normalize_text(tool_text) |
| if not tool_correct and expected_value is not None: |
| tool_correct = _compare_numeric(expected_value, tool_num) |
|
|
| if tool_correct: |
| correct_tool += 1 |
|
|
| baseline_correct = None |
| baseline_response = None |
| if run_baseline: |
| baseline_response = wrapper.generate_baseline(prompt) |
| baseline_num = _extract_last_number(baseline_response.get("response", "")) |
| if dataset_name == "gsm8k": |
| baseline_correct = _compare_numeric(expected_value, baseline_num) |
| else: |
| baseline_correct = _normalize_text(expected_text) == _normalize_text( |
| baseline_response.get("response", "") |
| ) |
| if not baseline_correct and expected_value is not None: |
| baseline_correct = _compare_numeric(expected_value, baseline_num) |
| if baseline_correct: |
| correct_baseline += 1 |
|
|
| results.append( |
| { |
| "prompt": prompt, |
| "expected": expected_text, |
| "tool_response": tool_response, |
| "tool_correct": tool_correct, |
| "baseline_response": baseline_response, |
| "baseline_correct": baseline_correct, |
| } |
| ) |
|
|
| total = len(results) |
| tool_accuracy = (correct_tool / total * 100) if total else 0.0 |
| baseline_accuracy = (correct_baseline / total * 100) if total else None |
| tool_success_rate = (tool_success_count / total * 100) if total else 0.0 |
|
|
| return { |
| "dataset": dataset_name, |
| "split": split, |
| "total": total, |
| "tool_accuracy": tool_accuracy, |
| "baseline_accuracy": baseline_accuracy, |
| "tool_success_rate": tool_success_rate, |
| "results": results, |
| } |
|
|
|
|
| def main() -> None: |
| args = parse_args() |
| output_dir = Path(args.output) |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| wrapper = create_wrapper( |
| model_path=args.model_path, |
| use_thinking=True, |
| temperature=0.6, |
| max_tokens=args.max_tokens, |
| tool_selection=args.tool_selection, |
| llm_query_extraction=args.llm_query_extraction, |
| transformers_model_path=args.transformers_model_path, |
| transformers_device=args.transformers_device, |
| transformers_trust_remote_code=args.transformers_trust_remote_code, |
| transformers_local_files_only=not args.allow_model_download, |
| verbose=args.verbose, |
| ) |
|
|
| wrapper.load_model() |
|
|
| start_time = time.time() |
| report = run_benchmark( |
| wrapper=wrapper, |
| dataset_name=args.dataset, |
| split=args.split, |
| limit=args.limit, |
| run_baseline=not args.no_baseline, |
| verbose=args.verbose, |
| ) |
| report["elapsed_seconds"] = time.time() - start_time |
| report["model_info"] = wrapper.get_model_info() |
|
|
| timestamp = time.strftime("%Y%m%d_%H%M%S") |
| output_path = output_dir / f"official_benchmark_{args.dataset}_{timestamp}.json" |
| output_path.write_text(json.dumps(report, indent=2)) |
|
|
| print("\nOfficial Benchmark Results") |
| print("=" * 60) |
| print(f"Dataset: {args.dataset} ({args.split})") |
| print(f"Samples: {report['total']}") |
| print(f"Tool Accuracy: {report['tool_accuracy']:.1f}%") |
| if report["baseline_accuracy"] is not None: |
| print(f"Baseline Accuracy: {report['baseline_accuracy']:.1f}%") |
| print(f"Tool Success Rate: {report['tool_success_rate']:.1f}%") |
| print(f"Elapsed: {report['elapsed_seconds']:.1f}s") |
| print(f"Results saved to: {output_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|