| |
| """ |
| Unified Benchmark Runner for QAFD-RAG |
| ====================================== |
| |
| Supports two graph types (always with Query-Aware Flow Diffusion): |
| |
| - **passage-entity**: Entities + passages + facts as nodes, synonymy edges. |
| Flow diffusion reaches passages directly. Default for multihop. |
| |
| - **entity**: Classic KG with entity + relationship nodes. |
| Passages are looked up after graph traversal. Default for other tasks. |
| |
| Usage:: |
| |
| # Multihop (auto-selects passage-entity graph) |
| python benchmarks/run.py --task multihop --dataset musique |
| |
| # Override graph type |
| python benchmarks/run.py --task multihop --dataset musique --graph_type entity |
| |
| # Ultradomain (auto-selects entity graph) |
| python benchmarks/run.py --task ultradomain --dataset mix |
| |
| # Text2SQL |
| python benchmarks/run.py --task text2sql --dataset spider2-lite |
| |
| # Build KG only |
| python benchmarks/run.py --task multihop --dataset musique --build_only |
| """ |
|
|
| import argparse |
| import asyncio |
| import os |
| import sys |
|
|
| QAFD_RAG_HOME = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) |
| sys.path.insert(0, QAFD_RAG_HOME) |
|
|
| |
|
|
| TASK_DATASETS = { |
| "multihop": ["musique", "hotpotqa", "2wikimultihopqa"], |
| "ultradomain": [ |
| "agriculture", "biology", "cs", "finance", "legal", |
| "math", "medicine", "mix", "music", "philosophy", |
| "physics", "psychology", |
| ], |
| "text2sql": ["spider2-lite", "bird"], |
| "summarization": ["squality"], |
| } |
|
|
| |
|
|
| TASK_DEFAULT_GRAPH_TYPE = { |
| "multihop": "passage-entity", |
| "ultradomain": "entity", |
| "text2sql": "entity", |
| "summarization": "entity", |
| } |
|
|
| |
| |
|
|
| GRAPH_TYPE_DEFAULTS = { |
| "passage-entity": { |
| "alpha": 2.0, |
| "epsilon": 0.01, |
| "max_iterations": 500, |
| "step_size": 0.2, |
| "weight_scheme": "original", |
| "linking_top_k": 5, |
| "passage_node_weight": 0.05, |
| "retrieval_top_k": 200, |
| }, |
| "entity": { |
| "alpha": 2.0, |
| "epsilon": 0.01, |
| "max_iterations": 500, |
| "step_size": 0.2, |
| "weight_scheme": "original", |
| "max_source_nodes": 20, |
| "min_flow_threshold": 0.1, |
| }, |
| } |
|
|
|
|
| def run_passage_entity(args): |
| """Run benchmark using passage-entity graph (passage_entity).""" |
| |
| import types as _types |
| for _pkg_path in ["src", "src.retrievers", "src.passage_entity"]: |
| if _pkg_path not in sys.modules: |
| _m = _types.ModuleType(_pkg_path) |
| _m.__path__ = [os.path.join(QAFD_RAG_HOME, *_pkg_path.split("."))] |
| _m.__package__ = _pkg_path |
| sys.modules[_pkg_path] = _m |
|
|
| import importlib.util as _ilu |
| def _load_mod(fqn, filepath): |
| spec = _ilu.spec_from_file_location(fqn, filepath) |
| mod = _ilu.module_from_spec(spec) |
| sys.modules[fqn] = mod |
| spec.loader.exec_module(mod) |
| return mod |
|
|
| _src = os.path.join(QAFD_RAG_HOME, "src") |
| _load_mod("src.retrievers.base", os.path.join(_src, "retrievers", "base.py")) |
| _load_mod("src.retrievers.flow_diffusion", os.path.join(_src, "retrievers", "flow_diffusion.py")) |
|
|
| |
| from src.passage_entity.benchmark_runner import main as pe_main |
|
|
| |
| sub_argv = [ |
| "benchmark_runner", |
| "--task", args.task, |
| "--dataset", args.dataset, |
| "--data_dir", os.path.join(QAFD_RAG_HOME, "data", "multihop"), |
| "--embedding_model", args.embedding, |
| "--llm_model", args.llm, |
| "--num_queries", str(args.questions), |
| "--qafd_alpha", str(args.alpha), |
| "--qafd_epsilon", str(args.epsilon), |
| "--qafd_max_iterations", str(args.max_iterations), |
| "--qafd_step_size", str(args.step_size), |
| "--qafd_weight_scheme", str(args.weight_scheme), |
| "--linking_top_k", str(args.linking_top_k), |
| "--passage_node_weight", str(args.passage_node_weight), |
| "--retrieval_top_k", str(args.retrieval_top_k), |
| ] |
| if args.skip_qa: |
| sub_argv.append("--skip_qa") |
| if getattr(args, 'batch_push', False): |
| sub_argv.append("--batch_push") |
| if args.force_build: |
| sub_argv.append("--force_index") |
| sub_argv.append("--force_openie") |
| if args.max_documents: |
| sub_argv.extend(["--max_documents", str(args.max_documents)]) |
|
|
| old_argv = sys.argv |
| sys.argv = sub_argv |
| try: |
| pe_main() |
| finally: |
| sys.argv = old_argv |
|
|
|
|
| def run_entity(args): |
| """Run benchmark using entity graph (original QAFD-RAG pipeline).""" |
| import nest_asyncio |
| nest_asyncio.apply() |
|
|
| task = args.task |
|
|
| if task == "multihop": |
| from benchmarks.multihop.benchmark_multihop import MultiHopBenchmark |
|
|
| api_key = os.environ.get("OPENAI_API_KEY") |
| if not api_key: |
| print("ERROR: Set OPENAI_API_KEY environment variable") |
| return |
|
|
| benchmark = MultiHopBenchmark(args.dataset, api_key, args.embedding, args.llm) |
|
|
| if args.build_only: |
| asyncio.run(benchmark.build_kg(max_documents=args.max_documents)) |
| return |
|
|
| result = asyncio.run(benchmark.run_benchmark( |
| question_count=args.questions, |
| force_build=args.force_build, |
| max_documents=args.max_documents, |
| mode="hybrid", |
| max_source_nodes=args.max_source_nodes, |
| min_flow_threshold=args.min_flow_threshold, |
| alpha=args.alpha, |
| )) |
| benchmark.save_results(result) |
|
|
| elif task == "ultradomain": |
| |
| |
| ud_dataset = args.dataset if args.dataset.endswith(".jsonl") else f"{args.dataset}.jsonl" |
| sub_argv = [ |
| "benchmark_ultradomain", |
| "--dataset", ud_dataset, |
| "--questions", str(args.questions), |
| "--embedding", args.embedding, |
| "--llm", args.llm, |
| ] |
| if args.force_build: |
| sub_argv.append("--force-build") |
| if args.build_only: |
| sub_argv.append("--build") |
| if args.max_documents: |
| sub_argv.extend(["--max-documents", str(args.max_documents)]) |
|
|
| old_argv = sys.argv |
| sys.argv = sub_argv |
| try: |
| from benchmarks.ultradomain.benchmark_ultradomain import main as ultra_main |
| asyncio.run(ultra_main()) |
| finally: |
| sys.argv = old_argv |
|
|
| elif task == "text2sql": |
| sub_argv = ["benchmark_text2sql"] |
| if args.max_documents: |
| sub_argv.extend(["--max-documents", str(args.max_documents)]) |
|
|
| old_argv = sys.argv |
| sys.argv = sub_argv |
| try: |
| from benchmarks.text2sql.benchmark_text2sql import main as text2sql_main |
| text2sql_main() |
| finally: |
| sys.argv = old_argv |
|
|
| elif task == "summarization": |
| sub_argv = [ |
| "benchmark_summarization", |
| "--dataset", args.dataset, |
| "--questions", str(args.questions), |
| "--embedding", args.embedding, |
| "--llm", args.llm, |
| ] |
| if args.force_build: |
| sub_argv.append("--force-build") |
| if args.build_only: |
| sub_argv.append("--build") |
| if args.max_documents: |
| sub_argv.extend(["--max-documents", str(args.max_documents)]) |
|
|
| old_argv = sys.argv |
| sys.argv = sub_argv |
| try: |
| from benchmarks.summarization.benchmark_summarization import main as summ_main |
| asyncio.run(summ_main()) |
| finally: |
| sys.argv = old_argv |
|
|
| else: |
| print(f"ERROR: Unknown task '{task}'") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="QAFD-RAG Unified Benchmark Runner", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Multihop with passage-entity graph (default) |
| python benchmarks/run.py --task multihop --dataset musique |
| |
| # Multihop with entity graph (override) |
| python benchmarks/run.py --task multihop --dataset musique --graph_type entity |
| |
| # Ultradomain with entity graph (default) |
| python benchmarks/run.py --task ultradomain --dataset mix |
| |
| # Retrieval only (skip QA) |
| python benchmarks/run.py --task multihop --dataset musique --skip_qa |
| |
| # Build KG only |
| python benchmarks/run.py --task multihop --dataset musique --build_only |
| """, |
| ) |
|
|
| |
| parser.add_argument("--task", type=str, required=True, |
| choices=["multihop", "ultradomain", "text2sql", "summarization"]) |
| parser.add_argument("--dataset", type=str, required=True, |
| help="Dataset name (e.g. musique, hotpotqa, mix, spider2-lite)") |
|
|
| |
| parser.add_argument("--graph_type", type=str, default=None, |
| choices=["passage-entity", "entity"], |
| help="Graph type (default: passage-entity for multihop, entity for others)") |
|
|
| |
| parser.add_argument("--llm", type=str, default="gpt-4o-mini") |
| parser.add_argument("--embedding", type=str, default=None, |
| help="Embedding model (auto-selected per task: " |
| "nvidia-nv-embed-v2 for multihop, openai-small for others)") |
|
|
| |
| parser.add_argument("--questions", type=int, default=100) |
| parser.add_argument("--max_documents", type=int, default=None) |
| parser.add_argument("--build_only", action="store_true", |
| help="Build KG only, skip benchmark") |
| parser.add_argument("--force_build", action="store_true", |
| help="Rebuild KG even if it exists") |
| parser.add_argument("--skip_qa", action="store_true", |
| help="Run retrieval only, skip QA (passage-entity only)") |
| parser.add_argument("--batch_push", action="store_true", |
| help="Batch push-relabel (process all excess nodes per iter)") |
|
|
| |
| parser.add_argument("--alpha", type=float, default=None, |
| help="QAFD alpha (default: 2.0)") |
| parser.add_argument("--epsilon", type=float, default=None) |
| parser.add_argument("--max_iterations", type=int, default=None) |
| parser.add_argument("--step_size", type=float, default=None) |
| parser.add_argument("--weight_scheme", type=str, default=None, |
| choices=["original", "multiply", "add"]) |
|
|
| |
| parser.add_argument("--linking_top_k", type=int, default=None, |
| help="Number of fact seeds (passage-entity only)") |
| parser.add_argument("--passage_node_weight", type=float, default=None, |
| help="Passage node weight in seed computation (passage-entity only)") |
| parser.add_argument("--retrieval_top_k", type=int, default=None, |
| help="Number of passages to retrieve (passage-entity only)") |
|
|
| |
| parser.add_argument("--max_source_nodes", type=int, default=None, |
| help="Max source nodes for flow diffusion (entity only)") |
| parser.add_argument("--min_flow_threshold", type=float, default=None, |
| help="Min flow threshold for clusters (entity only)") |
|
|
| args = parser.parse_args() |
|
|
| |
| if args.graph_type is None: |
| args.graph_type = TASK_DEFAULT_GRAPH_TYPE[args.task] |
|
|
| |
| if args.embedding is None: |
| _task_embeddings = { |
| "multihop": "nvidia-nv-embed-v2", |
| "ultradomain": "openai-small", |
| "text2sql": "openai-small", |
| "summarization": "openai-small", |
| } |
| args.embedding = _task_embeddings[args.task] |
|
|
| |
| valid = TASK_DATASETS.get(args.task, []) |
| if args.dataset not in valid and args.dataset != "all": |
| print(f"ERROR: Unknown dataset '{args.dataset}' for task '{args.task}'") |
| print(f" Valid: {valid}") |
| return |
|
|
| |
| defaults = GRAPH_TYPE_DEFAULTS[args.graph_type] |
| for key, default_val in defaults.items(): |
| if getattr(args, key, None) is None: |
| setattr(args, key, default_val) |
|
|
| |
| print(f"\n{'=' * 70}") |
| print(f" QAFD-RAG Benchmark") |
| print(f"{'=' * 70}") |
| print(f" Task: {args.task}") |
| print(f" Dataset: {args.dataset}") |
| print(f" Graph type: {args.graph_type}") |
| print(f" LLM: {args.llm}") |
| print(f" Embedding: {args.embedding}") |
| print(f" QAFD alpha: {args.alpha}") |
| print(f" Questions: {args.questions}") |
| print(f"{'=' * 70}\n") |
|
|
| |
| if args.graph_type == "passage-entity": |
| run_passage_entity(args) |
| else: |
| run_entity(args) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|