William Arnold commited on
Commit
85a9b4c
·
0 Parent(s):

New eval/plot pipeline

Browse files
.gitignore ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv
2
+ lm-outputs*
3
+ lmo
4
+ *.png
5
+ .vscode/launch.json
6
+ scratch
7
+
8
+ # Created by https://www.toptal.com/developers/gitignore/api/python
9
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
10
+
11
+ ### Python ###
12
+ # Byte-compiled / optimized / DLL files
13
+ __pycache__/
14
+ *.py[cod]
15
+ *$py.class
16
+
17
+ # C extensions
18
+ *.so
19
+
20
+ # Distribution / packaging
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ share/python-wheels/
35
+ *.egg-info/
36
+ .installed.cfg
37
+ *.egg
38
+ MANIFEST
39
+
40
+ # PyInstaller
41
+ # Usually these files are written by a python script from a template
42
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
43
+ *.manifest
44
+ *.spec
45
+
46
+ # Installer logs
47
+ pip-log.txt
48
+ pip-delete-this-directory.txt
49
+
50
+ # Unit test / coverage reports
51
+ htmlcov/
52
+ .tox/
53
+ .nox/
54
+ .coverage
55
+ .coverage.*
56
+ .cache
57
+ nosetests.xml
58
+ coverage.xml
59
+ *.cover
60
+ *.py,cover
61
+ .hypothesis/
62
+ .pytest_cache/
63
+ cover/
64
+
65
+ # Translations
66
+ *.mo
67
+ *.pot
68
+
69
+ # Django stuff:
70
+ *.log
71
+ local_settings.py
72
+ db.sqlite3
73
+ db.sqlite3-journal
74
+
75
+ # Flask stuff:
76
+ instance/
77
+ .webassets-cache
78
+
79
+ # Scrapy stuff:
80
+ .scrapy
81
+
82
+ # Sphinx documentation
83
+ docs/_build/
84
+
85
+ # PyBuilder
86
+ .pybuilder/
87
+ target/
88
+
89
+ # Jupyter Notebook
90
+ .ipynb_checkpoints
91
+
92
+ # IPython
93
+ profile_default/
94
+ ipython_config.py
95
+
96
+ # pyenv
97
+ # For a library or package, you might want to ignore these files since the code is
98
+ # intended to run in multiple environments; otherwise, check them in:
99
+ # .python-version
100
+
101
+ # pipenv
102
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
103
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
104
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
105
+ # install all needed dependencies.
106
+ #Pipfile.lock
107
+
108
+ # poetry
109
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
110
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
111
+ # commonly ignored for libraries.
112
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
113
+ #poetry.lock
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ #pdm.lock
118
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
119
+ # in version control.
120
+ # https://pdm.fming.dev/#use-with-ide
121
+ .pdm.toml
122
+
123
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
124
+ __pypackages__/
125
+
126
+ # Celery stuff
127
+ celerybeat-schedule
128
+ celerybeat.pid
129
+
130
+ # SageMath parsed files
131
+ *.sage.py
132
+
133
+ # Environments
134
+ .env
135
+ .venv
136
+ env/
137
+ venv/
138
+ ENV/
139
+ env.bak/
140
+ venv.bak/
141
+
142
+ # Spyder project settings
143
+ .spyderproject
144
+ .spyproject
145
+
146
+ # Rope project settings
147
+ .ropeproject
148
+
149
+ # mkdocs documentation
150
+ /site
151
+
152
+ # mypy
153
+ .mypy_cache/
154
+ .dmypy.json
155
+ dmypy.json
156
+
157
+ # Pyre type checker
158
+ .pyre/
159
+
160
+ # pytype static type analyzer
161
+ .pytype/
162
+
163
+ # Cython debug symbols
164
+ cython_debug/
165
+
166
+ # PyCharm
167
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
168
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
169
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
170
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
171
+ #.idea/
172
+
173
+ ### Python Patch ###
174
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
175
+ poetry.toml
176
+
177
+ # ruff
178
+ .ruff_cache/
179
+
180
+ # LSP config files
181
+ pyrightconfig.json
182
+
183
+ # End of https://www.toptal.com/developers/gitignore/api/python
.pre-commit-config.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v2.3.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: check-toml
7
+ - id: end-of-file-fixer
8
+ - id: trailing-whitespace
9
+ - repo: https://github.com/astral-sh/ruff-pre-commit
10
+ rev: v0.5.5
11
+ hooks:
12
+ - id: ruff
13
+ - id: ruff-format
pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68", "setuptools_scm[toml]>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rbeval"
7
+ requires-python = ">=3.8"
8
+ dynamic = ["version"]
9
+ dependencies = [
10
+ "seaborn>=0.13.2"
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ eval = [
15
+ "lm-eval[vllm]==0.4.3"
16
+ ]
17
+
18
+ # Enables the usage of setuptools_scm
19
+ [tool.setuptools_scm]
src/rbeval/__init__.py ADDED
File without changes
src/rbeval/eval.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import argparse
3
+ from typing import Optional
4
+ import torch
5
+ import warnings
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from rbeval.eval_spec import EvalSpec, rand_uid
10
+
11
+
12
+ def run_lm_eval(
13
+ lm_eval_path: Optional[str],
14
+ model_args: str,
15
+ tasks: str,
16
+ num_fewshot: int,
17
+ output_path: str,
18
+ ):
19
+ lm_eval_path = lm_eval_path or "lm_eval"
20
+ cmd = [lm_eval_path]
21
+ cmd += ["--model_args", model_args]
22
+ cmd += ["--tasks", tasks]
23
+ cmd += ["--num_fewshot", num_fewshot]
24
+ cmd += ["--output_path", output_path]
25
+ cmd += ["--log_samples"]
26
+ cmd += ["--cache_requests", "true"]
27
+ cmd += ["--cache_requests", "true"]
28
+ print(f"Running: {' '.join(cmd)}")
29
+ subprocess.check_call(cmd, env=os.environ)
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(description="Run eval for a given model")
34
+
35
+ parser.add_argument("output", type=str, help="output directory")
36
+ parser.add_argument("model", type=str, help="model path")
37
+ parser.add_argument("group", type=str, default=None)
38
+
39
+ parser.add_argument("--lm_eval_path", type=str)
40
+ parser.add_argument("--req_cache_path", type=str, default="/tmp/lm_eval_cache")
41
+ parser.add_argument("--tasks", type=str, default="mmlu")
42
+ parser.add_argument("--min_fewshot", type=int, default=0)
43
+ parser.add_argument("--max_fewshot", type=int, default=0)
44
+ parser.add_argument("-r", "--reformat", type=str)
45
+
46
+ args = parser.parse_args()
47
+ model: str = args.model
48
+ output_path: Path = Path(args.output)
49
+ group: Optional[str] = args.group
50
+ reformat: Optional[str] = args.reformat
51
+ lm_eval_path: Optional[str] = args.lm_eval_path
52
+ req_cache_path = Path(args.req_cache_path)
53
+ tasks: str = args.tasks
54
+ min_fewshot: int = args.min_fewshot
55
+ max_fewshot: int = args.max_fewshot
56
+ max_fewshot = max(min_fewshot, max_fewshot)
57
+
58
+ if not output_path.exists():
59
+ warnings.warn(f"Output path {output_path} does not exist, creating it")
60
+ output_path.mkdir(parents=True)
61
+ if not req_cache_path.exists():
62
+ warnings.warn(f"Request cache path, {str(req_cache_path)} does not exist")
63
+
64
+ os.environ["LM_HARNESS_CACHE_PATH"] = args.req_cache_path
65
+
66
+ n_gpu = torch.cuda.device_count()
67
+ os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, range(n_gpu)))
68
+ model_args = f"pretrained={model},dtype=auto,gpu_memory_utilization=0.7,tensor_parallel_size=1,data_parallel_size={n_gpu},max_model_len=4096"
69
+ fewshot = list(range(min_fewshot, max_fewshot + 1))
70
+
71
+ for num_fewshot in fewshot:
72
+ cfg = EvalSpec(
73
+ uid=rand_uid(),
74
+ model=model,
75
+ model_name=model.split("/")[-1],
76
+ group=group,
77
+ model_args=model_args,
78
+ fewshot=num_fewshot,
79
+ tasks=tasks,
80
+ )
81
+ spec_name = cfg.name()
82
+ lm_eval_output_path = output_path / spec_name
83
+ if reformat:
84
+ ref_path = Path(reformat)
85
+ assert ref_path.exists()
86
+ ref_files = list(ref_path.glob("**/*.json*"))
87
+ print(f"Found dir to reformat with {len(ref_files)} files")
88
+ assert len(ref_files) > 0
89
+ lm_eval_output_path.mkdir(parents=True, exist_ok=False)
90
+ for file in ref_files:
91
+ file.rename(lm_eval_output_path / file.name)
92
+ else:
93
+ run_lm_eval(
94
+ lm_eval_path, model_args, tasks, num_fewshot, str(lm_eval_output_path)
95
+ )
96
+
97
+ # Succeeded, write config
98
+ cfg_path = output_path / f"{spec_name}.json"
99
+ with open(cfg_path, "w") as f:
100
+ f.write(cfg.json())
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
src/rbeval/eval_spec.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, asdict
2
+ import json
3
+ import uuid
4
+
5
+
6
+ def rand_uid():
7
+ return uuid.uuid4().hex[:8]
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class EvalSpec:
12
+ uid: str
13
+ model: str
14
+ model_name: str
15
+ group: str
16
+ model_args: str
17
+ fewshot: int
18
+ tasks: str
19
+
20
+ def json(self) -> str:
21
+ return json.dumps(asdict(self))
22
+
23
+ def name(self) -> str:
24
+ return (
25
+ f"{self.group}_{self.model_name}_fs{self.fewshot}_{self.tasks}_{self.uid}"
26
+ )
27
+
28
+ def pretty_name(self) -> str:
29
+ return f"{self.model_name} fs{self.fewshot}"
src/rbeval/plot/__main__.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+ import json
4
+ from typing import Dict, Optional
5
+ import numpy as np
6
+ import re
7
+ from rbeval.eval_spec import EvalSpec
8
+ from rbeval.plot.data import Eval, EvalGroup, ModelEval
9
+ from rbeval.plot.score_cdf import score_cdf
10
+ from tqdm import tqdm
11
+
12
+ plot_fns = [score_cdf]
13
+
14
+
15
+ def get_samples(inp: Path, name_filter: str) -> Dict[str, EvalGroup]:
16
+ groups: Dict[str, EvalGroup] = {}
17
+
18
+ for spec_file in (pbar := tqdm(list(inp.glob("*.json")), desc="Reading specs")):
19
+ pbar.set_description(f"Reading spec {spec_file.stem}")
20
+ with open(spec_file) as f:
21
+ spec = EvalSpec(**json.load(f))
22
+
23
+ if name_filter:
24
+ if re.match(name_filter, spec.model_name) is None:
25
+ print(f"Skipping spec {spec_file.stem}")
26
+ continue
27
+
28
+ group = groups.setdefault(spec.group, EvalGroup(group=spec.group))
29
+ model_eval = ModelEval(model_spec=spec)
30
+ group.model_evals.append(model_eval)
31
+ for samples_file in (spec_file.parent / spec_file.stem).glob(
32
+ "**/samples_*.json*"
33
+ ):
34
+ with open(samples_file, "r") as f:
35
+ if samples_file.suffix == ".jsonl":
36
+ docs = [json.loads(s) for s in f.readlines()]
37
+ else:
38
+ assert samples_file.suffix == ".json"
39
+ docs = json.load(f)
40
+
41
+ cor_logprobs = []
42
+ inc_logprobs = []
43
+ for doc in docs:
44
+ target = doc["target"]
45
+ probs = [float(a[0][0]) for a in doc["resps"]]
46
+ cor_logprobs.append(probs.pop(target))
47
+ inc_logprobs.append(probs)
48
+ model_eval.evals.append(
49
+ Eval(
50
+ name=samples_file.stem,
51
+ cor_logprobs=np.array(cor_logprobs),
52
+ inc_logprobs=np.array(inc_logprobs),
53
+ )
54
+ )
55
+
56
+ return groups
57
+
58
+
59
+ def main():
60
+ parser = argparse.ArgumentParser(description="Generate performance curves")
61
+ parser.add_argument("eval_dir", type=str)
62
+ parser.add_argument("figure_dir", type=str)
63
+ parser.add_argument("-n", "--name", type=str)
64
+ args, rest = parser.parse_known_args()
65
+
66
+ name_filter: Optional[str] = args.name
67
+ eval_dir = Path(args.eval_dir)
68
+ figure_dir = Path(args.figure_dir)
69
+ samples = get_samples(eval_dir, name_filter)
70
+
71
+ for fn in plot_fns:
72
+ fn(samples, figure_dir, rest)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
src/rbeval/plot/data.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import List
3
+
4
+ import numpy as np
5
+
6
+ from rbeval.eval_spec import EvalSpec
7
+
8
+
9
+ @dataclass
10
+ class Eval:
11
+ name: str
12
+ cor_logprobs: np.ndarray
13
+ """shape [n] array of correct logprobs"""
14
+ inc_logprobs: np.ndarray
15
+ """shape [n, k] array of incorrect logprobs"""
16
+
17
+
18
+ @dataclass
19
+ class ModelEval:
20
+ """The evaluations for a given model"""
21
+
22
+ model_spec: EvalSpec
23
+ evals: List[Eval] = field(default_factory=list)
24
+
25
+ @property
26
+ def model_name(self) -> str:
27
+ return self.model_spec.model_name
28
+
29
+
30
+ @dataclass
31
+ class EvalGroup:
32
+ """A group of model evals"""
33
+
34
+ group: str
35
+ model_evals: List[ModelEval] = field(default_factory=list)
src/rbeval/plot/score_cdf.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Dict, List
3
+
4
+ from matplotlib import pyplot as plt
5
+ from rbeval.plot.data import EvalGroup, ModelEval
6
+ from matplotlib import colormaps
7
+ from matplotlib.axes import Axes
8
+ import numpy as np
9
+
10
+
11
+ def score_cdf(samples: Dict[str, EvalGroup], figure_dir: Path, args: List[str]):
12
+ fig, axs = plt.subplots(
13
+ 1, len(samples), figsize=(5 * len(samples), 5), dpi=320, sharey=True
14
+ )
15
+
16
+ for ax, (group_name, group) in zip(axs, samples.items()):
17
+ group: EvalGroup
18
+
19
+ model_names = set(m.model_spec.model_name for m in group.model_evals)
20
+ max_fewshot = {}
21
+ for m in group.model_evals:
22
+ max_fewshot[m.model_name] = max(
23
+ max_fewshot.get(m.model_name, 0), m.model_spec.fewshot
24
+ )
25
+
26
+ scales = ["Purples", "Greens", "Oranges", "Reds"]
27
+ model_cmaps = {}
28
+ for i, (scale, n) in enumerate(zip(scales, model_names)):
29
+ mfs = max_fewshot[n]
30
+ if mfs > 0:
31
+ model_cmaps[n] = colormaps[scale](
32
+ np.linspace(0.4, 1, max_fewshot[n] + 1)
33
+ )
34
+ else:
35
+ model_cmaps[n] = colormaps[scale]([1.0])
36
+
37
+ for model_eval in group.model_evals:
38
+ spec = model_eval.model_spec
39
+ color = model_cmaps[spec.model_name][spec.fewshot]
40
+ plot_samples(
41
+ ax, model_eval, model_eval.model_spec.pretty_name(), color=color
42
+ )
43
+
44
+ label_ax(ax, title=group_name)
45
+
46
+ handles, labels = ax.get_legend_handles_labels()
47
+ # sort both labels and handles by labels
48
+ labels, handles = zip(*sorted(zip(labels, handles), key=lambda t: t[0]))
49
+ ax.legend(handles, labels)
50
+
51
+ fig.savefig(figure_dir / "score_cdf.png")
52
+
53
+
54
+ def label_ax(ax, title=True, y=True, x=True):
55
+ if x:
56
+ ax.set_xlabel("Model output probability")
57
+ if y:
58
+ ax.set_ylabel("Percent of samples with correct model output prob > p")
59
+ if title:
60
+ ax.set_title("Performance curve for mmlu")
61
+
62
+
63
+ def get_base_logits(probs):
64
+ logits = np.zeros(len(probs))
65
+ logits[0] = 1
66
+ rest = 1 - np.sum(probs)
67
+ assert (rest >= 0) and (rest <= 1)
68
+
69
+
70
+ def plot_samples(ax: Axes, meval: ModelEval, name: str, norm_by_stat=True, color=None):
71
+ bulk = np.concatenate([np.exp(e.cor_logprobs) for e in meval.evals])
72
+ num_cats = len(meval.evals)
73
+ weights = []
74
+ if norm_by_stat:
75
+ for e in meval.evals:
76
+ n = len(e.cor_logprobs)
77
+ # Each eval gets a total weight of 1/num_cats
78
+ # So each sample should have a weight of 1/num_cats/n
79
+ weights.append(np.ones(n) / (num_cats * n))
80
+ weights = np.concatenate(weights)
81
+ else:
82
+ weights = np.ones_like(bulk) / len(bulk)
83
+
84
+ sort_perm = bulk.argsort()
85
+ bulk = bulk[sort_perm]
86
+ weights = weights[sort_perm]
87
+ cdf_p = 1 - np.cumsum(weights)
88
+
89
+ ax.plot(bulk, cdf_p, label=name, color=color, alpha=0.8)
90
+ ax.set_xlim(0, 1)
91
+ ax.set_ylim(0, 1)