Skip to content

Commit 03b9b06

Browse files
hsiehjacksonfayejf
andauthored
Add GraphWalks Benchmark from OpenAI (#1455)
Signed-off-by: Cheng-Ping Hsieh <chsieh@nvidia.com> Signed-off-by: fayejf <fayejf07@gmail.com> Signed-off-by: Cheng-Ping Hsieh <37269846+hsiehjackson@users.noreply.github.com> Co-authored-by: fayejf <fayejf07@gmail.com>
1 parent 467b057 commit 03b9b06

7 files changed

Lines changed: 283 additions & 0 deletions

File tree

docs/evaluation/long-context.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ For more details see [https://github.com/NVIDIA/RULER/blob/rulerv2-ns](https://g
7272
- Benchmark is defined in [`nemo_skills/dataset/mrcr/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/mrcr/__init__.py)
7373
- Original benchmark source is [here](https://huggingface.co/datasets/openai/mrcr).
7474

75+
### graphwalks
76+
77+
- Benchmark is defined in [`nemo_skills/dataset/graphwalks/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/graphwalks/__init__.py)
78+
- Original benchmark source is [here](https://huggingface.co/datasets/openai/graphwalks).
79+
- We modify the original evaluation logic in this [discussion](https://huggingface.co/datasets/openai/graphwalks/discussions/8).
80+
7581
### aalcr
7682
- Benchmark is defined in [`nemo_skills/dataset/aalcr/__init__.py`](https://github.com/NVIDIA-NeMo/Skills/blob/main/nemo_skills/dataset/aalcr/__init__.py)
7783
- Original benchmark source is [here](https://huggingface.co/datasets/ArtificialAnalysis/AA-LCR) and the reported scores by AA is here [here](https://artificialanalysis.ai/evaluations/artificial-analysis-long-context-reasoning).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
EVAL_SPLIT = "all"
15+
METRICS_TYPE = "graphwalks"
16+
GENERATION_ARGS = "++prompt_format=openai ++eval_type=graphwalks"
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import argparse
15+
import json
16+
import re
17+
from pathlib import Path
18+
from typing import Callable
19+
20+
import tiktoken
21+
from datasets import load_dataset
22+
from tqdm import tqdm
23+
24+
"""
25+
Usage
26+
# default setup is "all" (all problem types, no context window filter)
27+
python prepare.py
28+
29+
# prepare only "parents" problem type
30+
python prepare.py --problem_types parents --setup parents
31+
32+
# prepare with a 128k context window limit
33+
python prepare.py --max_context_window 131072 --setup 128k
34+
35+
# prepare BFS problems within 128k context
36+
python prepare.py --max_context_window 131072 --problem_types bfs --setup bfs_128k
37+
38+
# use a HuggingFace tokenizer instead of tiktoken
39+
python prepare.py --tokenizer meta-llama/Llama-3.1-8B-Instruct --max_context_window 131072 --setup 128k
40+
"""
41+
42+
43+
def build_tokenizer(tokenizer_name: str) -> Callable[[str], int]:
44+
"""Return a callable that counts tokens in a string.
45+
46+
Tries tiktoken first; if the name is not a valid tiktoken encoding,
47+
falls back to a HuggingFace AutoTokenizer.
48+
"""
49+
try:
50+
enc = tiktoken.get_encoding(tokenizer_name)
51+
return lambda text: len(enc.encode(text))
52+
except ValueError:
53+
from transformers import AutoTokenizer
54+
55+
hf_tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
56+
return lambda text: len(hf_tokenizer.encode(text, add_special_tokens=False))
57+
58+
59+
def write_data_to_file(output_file, data, max_context_window, problem_types, count_tokens: Callable[[str], int]):
60+
with open(output_file, "wt", encoding="utf-8") as fout:
61+
for idx, entry in tqdm(enumerate(data), desc=f"Writing {output_file.name}"):
62+
if problem_types is not None and entry["problem_type"] not in problem_types:
63+
print(f"Skipping {idx} because problem_type={entry['problem_type']} not in {problem_types}")
64+
continue
65+
66+
prompt_text = entry["prompt"]
67+
answer_nodes = entry["answer_nodes"]
68+
69+
pattern = r"Perform a BFS from node (\S+) with depth (\d+)"
70+
replacement = r"Perform a BFS from node \1 and return only the nodes at exactly depth \2 (not nodes at intermediate depths)"
71+
prompt_text = re.sub(pattern, replacement, prompt_text)
72+
73+
m = re.search(r"Find the parents of node ([^\s.]+)\.", prompt_text)
74+
node_id = m.group(1) if m else None
75+
if node_id is not None and node_id in answer_nodes:
76+
answer_nodes.remove(node_id)
77+
print(f"Removing {idx} sample with node {node_id} from answer_nodes because it is in the prompt")
78+
79+
n_tokens = count_tokens(prompt_text)
80+
81+
if max_context_window is not None and n_tokens > max_context_window:
82+
print(f"Skipping {idx} because it has {n_tokens} tokens (limit={max_context_window})")
83+
continue
84+
85+
messages = [{"role": "user", "content": prompt_text}]
86+
87+
output_entry = {
88+
"messages": messages,
89+
"expected_answer": json.dumps(sorted(answer_nodes)),
90+
"n_tokens": n_tokens,
91+
"prompt_chars": entry["prompt_chars"],
92+
"problem_type": entry["problem_type"],
93+
}
94+
json.dump(output_entry, fout)
95+
fout.write("\n")
96+
97+
98+
def get_graphwalks_data(problem_types, setup, max_context_window, tokenizer_name):
99+
dataset = load_dataset("openai/graphwalks")["train"]
100+
data_dir = Path(__file__).absolute().parent
101+
102+
count_tokens = build_tokenizer(tokenizer_name)
103+
output_file = data_dir / f"{setup}.jsonl"
104+
write_data_to_file(output_file, dataset, max_context_window, problem_types, count_tokens)
105+
106+
107+
if __name__ == "__main__":
108+
parser = argparse.ArgumentParser(description="Prepare GraphWalks dataset.")
109+
parser.add_argument(
110+
"--max_context_window",
111+
type=int,
112+
default=None,
113+
help="Maximum context window size in tokens. Samples exceeding this will be skipped.",
114+
)
115+
parser.add_argument(
116+
"--problem_types",
117+
nargs="+",
118+
type=str,
119+
default=None,
120+
help="Problem types to include (e.g. parents bfs). Defaults to all types.",
121+
)
122+
parser.add_argument(
123+
"--setup",
124+
type=str,
125+
default="all",
126+
help="Setup name used as the output filename, e.g. 'all', 'parents', 'bfs_128k'.",
127+
)
128+
parser.add_argument(
129+
"--tokenizer_name",
130+
type=str,
131+
default="cl100k_base",
132+
help=(
133+
"Tokenizer to use for counting tokens. Pass a tiktoken encoding name "
134+
"(e.g. 'cl100k_base', 'o200k_base') or a HuggingFace model id / local path "
135+
"(e.g. 'meta-llama/Llama-3.1-8B-Instruct')."
136+
),
137+
)
138+
139+
args = parser.parse_args()
140+
141+
print(f"Preparing GraphWalks dataset with arguments: {args}")
142+
get_graphwalks_data(args.problem_types, args.setup, args.max_context_window, args.tokenizer_name)
143+
print(f"GraphWalks dataset preparation with setup '{args.setup}' completed. Use --split={args.setup} to evaluate!")

nemo_skills/evaluation/evaluator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"livecodebench_pro": "nemo_skills.evaluation.evaluator.code:eval_livecodebench_pro",
4040
"scicode": "nemo_skills.evaluation.evaluator.scicode:eval_scicode",
4141
"mrcr": "nemo_skills.evaluation.evaluator.mrcr:eval_mrcr",
42+
"graphwalks": "nemo_skills.evaluation.evaluator.graphwalks:eval_graphwalks",
4243
"bigcodebench": "nemo_skills.evaluation.evaluator.code:eval_bigcodebench",
4344
"human_eval_infilling": "nemo_skills.evaluation.evaluator.code:eval_human_eval_infilling",
4445
"mmau-pro": "nemo_skills.evaluation.evaluator.mmau_pro:eval_mmau_pro",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import logging
17+
import re
18+
19+
from tqdm import tqdm
20+
21+
from nemo_skills.evaluation.evaluator.base import BaseEvaluatorConfig
22+
from nemo_skills.utils import get_logger_name
23+
24+
LOG = logging.getLogger(get_logger_name(__file__))
25+
26+
27+
def get_list(response: str) -> tuple[list[str], bool]:
28+
"""Parse the predicted node list from the last non-empty line of the response.
29+
30+
Expects the format: ``Final Answer: [node1, node2, ...]``
31+
32+
Returns:
33+
(nodes, parse_failed) where parse_failed is True when the expected
34+
format was not found.
35+
36+
Reference: https://huggingface.co/datasets/openai/graphwalks
37+
"""
38+
lines = [line for line in response.strip().split("\n") if line.strip()]
39+
if not lines:
40+
return [], True
41+
42+
last_line = lines[-1]
43+
match = re.search(r"Final Answer:\s*\[(.*)\]", last_line)
44+
if match:
45+
content = match.group(1)
46+
if not content.strip():
47+
return [], False
48+
return [item.strip() for item in content.split(",") if item.strip()], False
49+
50+
return [], True
51+
52+
53+
def eval_graphwalks(cfg):
54+
cfg = BaseEvaluatorConfig(**cfg)
55+
56+
jsonl_file = cfg.input_file
57+
with open(jsonl_file, "rt", encoding="utf-8") as fin:
58+
data = [json.loads(line) for line in fin]
59+
60+
with open(jsonl_file, "wt", encoding="utf-8") as fout:
61+
for sample in tqdm(data):
62+
predicted_list, parse_failed = get_list(sample["generation"])
63+
predicted_nodes = set(predicted_list)
64+
65+
try:
66+
expected_nodes = set(json.loads(sample["expected_answer"]))
67+
except (json.JSONDecodeError, TypeError):
68+
expected_nodes = set()
69+
70+
if not expected_nodes and not predicted_nodes:
71+
f1 = 1.0
72+
elif not predicted_nodes or not expected_nodes:
73+
f1 = 0.0
74+
else:
75+
tp = len(predicted_nodes & expected_nodes)
76+
precision = tp / len(predicted_nodes)
77+
recall = tp / len(expected_nodes)
78+
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
79+
80+
sample["f1"] = f1
81+
sample["parse_failed"] = parse_failed
82+
fout.write(json.dumps(sample) + "\n")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from nemo_skills.evaluation.metrics.base import BaseMetrics
16+
17+
18+
class GraphWalksMetrics(BaseMetrics):
19+
"""Metrics for the GraphWalks benchmark (openai/graphwalks).
20+
21+
Reads pre-computed fields written by eval_graphwalks in the evaluator.
22+
23+
Metrics reported:
24+
- ``f1``: set-level F1 between predicted and expected node sets.
25+
- ``parse_failed``: 1.0 iff the parse failed.
26+
"""
27+
28+
def _get_score_dict(self, prediction: dict) -> dict[str, bool | int | float]:
29+
return {"f1": prediction["f1"], "parse_failed": prediction["parse_failed"]}
30+
31+
def update(self, predictions):
32+
super().update(predictions)
33+
self._compute_pass_at_k(predictions=predictions)

nemo_skills/evaluation/metrics/map_metrics.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from nemo_skills.evaluation.metrics.contextasr_metrics import ContextASRMetrics
3434
from nemo_skills.evaluation.metrics.critpt_metrics import CritPtMetrics
3535
from nemo_skills.evaluation.metrics.gradingbench_metrics import GradingBenchMetrics
36+
from nemo_skills.evaluation.metrics.graphwalks_metrics import GraphWalksMetrics
3637
from nemo_skills.evaluation.metrics.hleaa_metrics import HLEAAMetrics
3738
from nemo_skills.evaluation.metrics.hotpotqa_metrics import HotpotQAMetrics
3839
from nemo_skills.evaluation.metrics.icpc_metrics import ICPCMetrics
@@ -102,6 +103,7 @@
102103
"contextasr": ContextASRMetrics,
103104
"hotpotqa": HotpotQAMetrics,
104105
"hotpotqa_closedbook": functools.partial(HotpotQAMetrics, closed_book=True),
106+
"graphwalks": GraphWalksMetrics,
105107
"weighted-math": WeightedMathMetrics,
106108
}
107109

0 commit comments

Comments
 (0)