".circleci/unittest/windows/scripts/run_test.sh" did not exist on "6bc591ec7995481a483079a8b71f5c923e8e0ace"
__init__.py 19.4 KB
Newer Older
1
import collections
2
3
import logging
import os
4
from functools import partial
5
from typing import Dict, List, Mapping, Optional, Union
&'s avatar
& committed
6

7
from lm_eval import utils
8
9
from lm_eval.api.task import ConfigurableGroup, ConfigurableTask, GroupConfig, Task

10
11

GROUP_ONLY_KEYS = list(GroupConfig().to_dict().keys())
lintangsutawika's avatar
lintangsutawika committed
12

13

14
15
16
class TaskManager:
    """TaskManager indexes all tasks from the default `lm_eval/tasks/`
    and an optional directory if provided.
17

18
19
    """

20
21
22
23
24
25
    def __init__(
        self,
        verbosity="INFO",
        include_path: Optional[Union[str, List]] = None,
        include_defaults: bool = True,
    ) -> None:
26
27
28
29
30
        self.verbosity = verbosity
        self.include_path = include_path
        self.logger = utils.eval_logger
        self.logger.setLevel(getattr(logging, f"{verbosity}"))

31
32
33
        self._task_index = self.initialize_tasks(
            include_path=include_path, include_defaults=include_defaults
        )
34
        self._all_tasks = sorted(list(self._task_index.keys()))
35

36
        self.task_group_map = collections.defaultdict(list)
37

38
39
40
41
42
    def initialize_tasks(
        self,
        include_path: Optional[Union[str, List]] = None,
        include_defaults: bool = True,
    ):
43
        """Creates a dictionary of tasks index.
44

45
46
47
48
49
        :param include_path: Union[str, List] = None
            An additional path to be searched for tasks recursively.
            Can provide more than one such path as a list.
        :param include_defaults: bool = True
            If set to false, default tasks (those in lm_eval/tasks/) are not indexed.
50
51
52
        :return
            Dictionary of task names as key and task metadata
        """
53
54
55
56
        if include_defaults:
            all_paths = [os.path.dirname(os.path.abspath(__file__)) + "/"]
        else:
            all_paths = []
57
58
59
60
        if include_path is not None:
            if isinstance(include_path, str):
                include_path = [include_path]
            all_paths.extend(include_path)
61

62
63
64
65
        task_index = {}
        for task_dir in all_paths:
            tasks = self._get_task_and_group(task_dir)
            task_index = {**tasks, **task_index}
lintangsutawika's avatar
format  
lintangsutawika committed
66

67
68
69
70
71
72
73
74
75
76
77
        return task_index

    @property
    def all_tasks(self):
        return self._all_tasks

    @property
    def task_index(self):
        return self._task_index

    def match_tasks(self, task_list):
78
        return utils.pattern_match(task_list, self.all_tasks)
79

80
    def _name_is_registered(self, name) -> bool:
81
82
83
84
        if name in self.all_tasks:
            return True
        return False

85
    def _name_is_task(self, name) -> bool:
lintangsutawika's avatar
lintangsutawika committed
86
        if self._name_is_registered(name) and (self.task_index[name]["type"] == "task"):
87
88
89
            return True
        return False

lintangsutawika's avatar
lintangsutawika committed
90
    def _name_is_tag(self, name) -> bool:
lintangsutawika's avatar
lintangsutawika committed
91
        if self._name_is_registered(name) and (self.task_index[name]["type"] == "tag"):
lintangsutawika's avatar
lintangsutawika committed
92
93
94
            return True
        return False

95
    def _name_is_group(self, name) -> bool:
96
97
98
        if self._name_is_registered(name) and (
            self.task_index[name]["type"] == "group"
        ):
99
100
101
102
            return True
        return False

    def _name_is_python_task(self, name):
103
104
105
        if self._name_is_registered(name) and (
            self.task_index[name]["type"] == "python_task"
        ):
106
107
108
            return True
        return False

109
    def _config_is_task(self, config) -> bool:
110
111
112
113
        if ("task" in config) and isinstance(config["task"], str):
            return True
        return False

114
    def _config_is_group(self, config) -> bool:
115
116
117
118
        if ("task" in config) and isinstance(config["task"], list):
            return True
        return False

119
    def _config_is_python_task(self, config) -> bool:
120
121
122
123
124
        if "class" in config:
            return True
        return False

    def _get_yaml_path(self, name):
125
126
        if name not in self.task_index:
            raise ValueError
127
128
129
        return self.task_index[name]["yaml_path"]

    def _get_config(self, name):
130
131
        if name not in self.task_index:
            raise ValueError
132
133
134
135
136
137
138
        yaml_path = self._get_yaml_path(name)
        if yaml_path == -1:
            return {}
        else:
            return utils.load_yaml_config(yaml_path, mode="full")

    def _get_tasklist(self, name):
139
140
        if self._name_is_task(name):
            raise ValueError
141
142
143
144
145
146
147
148
149
150
151
152
        return self.task_index[name]["task"]

    def _process_alias(self, config, group=None):
        # If the group is not the same as the original
        # group which the group alias was intended for,
        # Set the group_alias to None instead.
        if ("group_alias" in config) and ("group" in config) and group is not None:
            if config["group"] != group:
                config["group_alias"] = None
        return config

    def _load_individual_task_or_group(
153
        self,
154
155
156
157
        name_or_config: Optional[Union[str, dict]] = None,
        parent_name: Optional[str] = None,
        update_config: Optional[dict] = None,
    ) -> Mapping:
158
        def _load_task(config, task):
159
            if "include" in config:
160
161
                config = {
                    **utils.load_yaml_config(
162
                        yaml_path=None,
163
164
                        yaml_config={"include": config.pop("include")},
                        mode="full",
165
166
167
                    ),
                    **config,
                }
168
            if self._config_is_python_task(config):
169
                task_object = config["class"](config=config)
170
171
            else:
                task_object = ConfigurableTask(config=config)
172
173
174
175

            if task != task_object.task_id:
                task_object.task_id = task

176
177
            return {task: task_object}

178
179
180
181
        def _get_group_and_subtask_from_config(config):
            group_name = ConfigurableGroup(config=config)
            subtask_list = []
            for task in group_name.config["task"]:
lintangsutawika's avatar
lintangsutawika committed
182
                if isinstance(task, str) and self._name_is_tag(task):
183
184
185
186
187
                    subtask_list.extend(self._get_tasklist(task))
                else:
                    subtask_list.append(task)
            return group_name, subtask_list

lintangsutawika's avatar
lintangsutawika committed
188
        def _process_group_config(config, update_config=None):
lintangsutawika's avatar
lintangsutawika committed
189
190
191
192
193
194
195
            if update_config is not None:
                config = {**config, **update_config}
            _update_config = {
                k: v for k, v in config.items() if k not in GROUP_ONLY_KEYS
            }
            if not bool(_update_config):
                _update_config = None
lintangsutawika's avatar
lintangsutawika committed
196
197

            group_config = {k: v for k, v in config.items() if k in GROUP_ONLY_KEYS}
lintangsutawika's avatar
lintangsutawika committed
198
            return group_config, _update_config
lintangsutawika's avatar
lintangsutawika committed
199

200
201
202
203
204
205
        if isinstance(name_or_config, str):
            if update_config is not None:
                # Process name_or_config as a dict instead
                name_or_config = {"task": name_or_config, **update_config}
            elif self._name_is_task(name_or_config):
                task_config = self._get_config(name_or_config)
206
                return _load_task(task_config, task=name_or_config)
207
            else:
208
209
210
                subtask_list = self._get_tasklist(name_or_config)
                if subtask_list == -1:
                    group_config = self._get_config(name_or_config)
lintangsutawika's avatar
lintangsutawika committed
211
                    group_config, update_config = _process_group_config(group_config)
lintangsutawika's avatar
lintangsutawika committed
212
213
214
                    group_name, subtask_list = _get_group_and_subtask_from_config(
                        group_config
                    )
lintangsutawika's avatar
lintangsutawika committed
215
                else:
216
217
218
                    if self._name_is_tag(name_or_config):
                        fn = partial(
                            self._load_individual_task_or_group,
lintangsutawika's avatar
lintangsutawika committed
219
220
221
222
223
224
                            update_config=name_or_config
                            if isinstance(name_or_config, dict)
                            else None,
                        )
                        return dict(
                            collections.ChainMap(*map(fn, reversed(subtask_list)))
225
226
227
228
229
                        )
                    else:
                        group_name = ConfigurableGroup(
                            config={"group": name_or_config, "task": subtask_list}
                        )
230

231
232
        if isinstance(name_or_config, dict):
            if self._config_is_task(name_or_config):
lintangsutawika's avatar
lintangsutawika committed
233
234
235
                name = name_or_config.pop("task")
                if update_config is not None:
                    name_or_config = {**name_or_config, **update_config}
236
                # If the name is registered as a group
lintangsutawika's avatar
lintangsutawika committed
237
238
239
240
241
242
243
244
245
246
                if self._name_is_group(name):
                    group_config = self._get_config(name)

                    group_config, update_config = _process_group_config(
                        group_config, name_or_config
                    )
                    group_name, subtask_list = _get_group_and_subtask_from_config(
                        group_config
                    )
                elif self._name_is_tag(name):
247
                    subtask_list = self._get_tasklist(name)
lintangsutawika's avatar
lintangsutawika committed
248
249
250
251
252
                    fn = partial(
                        self._load_individual_task_or_group,
                        update_config=name_or_config,
                    )
                    return dict(collections.ChainMap(*map(fn, reversed(subtask_list))))
253
254
255
256
257
258
                else:
                    if self._name_is_registered(name):
                        base_task_config = self._get_config(name)

                        # Check if this is a duplicate.
                        if parent_name is not None:
259
260
261
262
263
264
265
266
                            num_duplicate = len(
                                list(
                                    filter(
                                        lambda x: x.startswith(name),
                                        self.task_group_map[parent_name],
                                    )
                                )
                            )
267
268
269
270
                            if num_duplicate > 0:
                                name = f"{name}-{num_duplicate}"
                            self.task_group_map[parent_name].append(name)

271
272
273
274
                        task_config = {
                            **base_task_config,
                            **name_or_config,
                        }
275
276
                    else:
                        task_config = name_or_config
277
                    return _load_task(task_config, task=name)
278
            else:
lintangsutawika's avatar
lintangsutawika committed
279
                group_config, update_config = _process_group_config(name_or_config)
lintangsutawika's avatar
lintangsutawika committed
280
281
282
                group_name, subtask_list = _get_group_and_subtask_from_config(
                    group_config
                )
283

284
285
286
287
288
        fn = partial(
            self._load_individual_task_or_group,
            parent_name=group_name,
            update_config=update_config,
        )
289
290
291
        return {
            group_name: dict(collections.ChainMap(*map(fn, reversed(subtask_list))))
        }
292

293
    def load_task_or_group(self, task_list: Optional[Union[str, list]] = None) -> dict:
294
        """Loads a dictionary of task objects from a list
295

296
297
        :param task_list: Union[str, list] = None
            Single string or list of string of task names to be loaded
298

299
300
301
302
303
        :return
            Dictionary of task objects
        """
        if isinstance(task_list, str):
            task_list = [task_list]
304

305
        all_loaded_tasks = dict(
306
            collections.ChainMap(*map(self._load_individual_task_or_group, task_list))
307
308
309
310
311
312
313
        )
        return all_loaded_tasks

    def load_config(self, config: Dict):
        return self._load_individual_task_or_group(config)

    def _get_task_and_group(self, task_dir: str):
314
        """Creates a dictionary of tasks index with the following metadata,
lintangsutawika's avatar
lintangsutawika committed
315
        - `type`, that can be either `task`, `python_task`, `group` or `tags`.
316
317
            `task` refer to regular task configs, `python_task` are special
            yaml files that only consists of `task` and `class` parameters.
lintangsutawika's avatar
lintangsutawika committed
318
319
            `group` are group configs. `tags` are labels that can be assigned
            to tasks to assist in sorting and calling tasks of certain themes.
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
        - `yaml_path`, path to the yaml file. If the entry is a `group` that
            was configured through a task config, the yaml_path will be -1
            and all subtasks will be listed in `task` (see below)
        - `task`, reserved for entries with `type` as `group`. This will list
            all subtasks. When a group config is created (as opposed to task
            config having `group` parameter set), this will be set to -1 to
            avoid recursive indexing. The whole list of subtasks will be loaded
            at evaluation.

        :param task_dir: str
            A directory to check for tasks

        :return
            Dictionary of task names as key and task metadata
        """
335
336
        # TODO: remove group in next release
        print_info = True
337
338
339
340
        ignore_dirs = [
            "__pycache__",
            ".ipynb_checkpoints",
        ]
341
        tasks_and_groups = collections.defaultdict()
342
343
        for root, dirs, file_list in os.walk(task_dir):
            dirs[:] = [d for d in dirs if d not in ignore_dirs]
344
345
346
347
348
349
350
351
352
353
354
355
356
357
            for f in file_list:
                if f.endswith(".yaml"):
                    yaml_path = os.path.join(root, f)
                    config = utils.load_yaml_config(yaml_path, mode="simple")
                    if self._config_is_python_task(config):
                        # This is a python class config
                        tasks_and_groups[config["task"]] = {
                            "type": "python_task",
                            "yaml_path": yaml_path,
                        }
                    elif self._config_is_group(config):
                        # This is a group config
                        tasks_and_groups[config["group"]] = {
                            "type": "group",
358
359
360
361
362
                            "task": -1,  # This signals that
                            # we don't need to know
                            # the task list for indexing
                            # as it can be loaded
                            # when called.
363
364
                            "yaml_path": yaml_path,
                        }
lintangsutawika's avatar
lintangsutawika committed
365

366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
                        # # Registered the level 1 tasks from a group config
                        # for config in config["task"]:
                        #     if isinstance(config, dict) and self._config_is_task(config):
                        #         task = config["task"]
                        #         tasks_and_groups[task] = {
                        #             "type": "task",
                        #             "yaml_path": yaml_path,
                        #             }

                    elif self._config_is_task(config):
                        # This is a task config
                        task = config["task"]
                        tasks_and_groups[task] = {
                            "type": "task",
                            "yaml_path": yaml_path,
381
                        }
382

383
384
385
386
387
388
                        # TODO: remove group in next release
                        for attr in ["tag", "group"]:
                            if attr in config:
                                if attr == "group" and print_info:
                                    self.logger.info(
                                        "`group` and `group_alias` will no longer be used in the next release of lm-eval. "
389
                                        "`tag` will be used to allow to call a collection of tasks just like `group`. "
390
391
392
393
                                        "`group` will be removed in order to not cause confusion with the new ConfigurableGroup "
                                        "which will be the offical way to create groups with addition of group-wide configuations."
                                    )
                                    print_info = False
394
                                    # attr = "tag"
395
396
397
398
399
400
401
402
403
404
405
406
407
408

                                attr_list = config[attr]
                                if isinstance(attr_list, str):
                                    attr_list = [attr_list]

                                for tag in attr_list:
                                    if tag not in tasks_and_groups:
                                        tasks_and_groups[tag] = {
                                            "type": "tag",
                                            "task": [task],
                                            "yaml_path": -1,
                                        }
                                    else:
                                        tasks_and_groups[tag]["task"].append(task)
409
410
411
412
                    else:
                        self.logger.debug(f"File {f} in {root} could not be loaded")

        return tasks_and_groups
lintangsutawika's avatar
lintangsutawika committed
413

414

415
416
417
418
419
420
421
def get_task_name_from_config(task_config: Dict[str, str]) -> str:
    if "task" in task_config:
        return task_config["task"]
    if "dataset_name" in task_config:
        return "{dataset_path}_{dataset_name}".format(**task_config)
    else:
        return "{dataset_path}".format(**task_config)
lintangsutawika's avatar
lintangsutawika committed
422

423

lintangsutawika's avatar
lintangsutawika committed
424
def get_task_name_from_object(task_object):
425
426
    if hasattr(task_object, "config"):
        return task_object._config["task"]
lintangsutawika's avatar
lintangsutawika committed
427
428
429
430
431
432
433
434
435

    # TODO: scrap this
    # this gives a mechanism for non-registered tasks to have a custom name anyways when reporting
    return (
        task_object.EVAL_HARNESS_NAME
        if hasattr(task_object, "EVAL_HARNESS_NAME")
        else type(task_object).__name__
    )

436
437

def get_task_dict(
438
    task_name_list: Union[str, List[Union[str, Dict, Task]]],
439
    task_manager: Optional[TaskManager] = None,
440
):
441
    """Creates a dictionary of task objects from either a name of task, config, or prepared Task object.
lintangsutawika's avatar
lintangsutawika committed
442

443
444
445
446
447
448
449
    :param task_name_list: List[Union[str, Dict, Task]]
        Name of model or LM object, see lm_eval.models.get_model
    :param task_manager: TaskManager = None
        A TaskManager object that stores indexed tasks. If not set,
        task_manager will load one. This should be set by the user
        if there are additional paths that want to be included
        via `include_path`
450

451
452
453
454
    :return
        Dictionary of task objects
    """
    task_name_from_string_dict = {}
455
456
457
    task_name_from_config_dict = {}
    task_name_from_object_dict = {}

458
    if isinstance(task_name_list, str):
lintangsutawika's avatar
lintangsutawika committed
459
        task_name_list = [task_name_list]
460
461
462
463
464
465
466
467
468
    elif isinstance(task_name_list, list):
        if not all([isinstance(task, (str, dict, Task)) for task in task_name_list]):
            raise TypeError(
                "Expected all list items to be of types 'str', 'dict', or 'Task', but at least one entry did not match."
            )
    else:
        raise TypeError(
            f"Expected a 'str' or 'list' but received {type(task_name_list)}."
        )
lintangsutawika's avatar
lintangsutawika committed
469

470
    string_task_name_list = [task for task in task_name_list if isinstance(task, str)]
471
472
473
    others_task_name_list = [
        task for task in task_name_list if not isinstance(task, str)
    ]
474
475
476
    if len(string_task_name_list) > 0:
        if task_manager is None:
            task_manager = TaskManager()
lintangsutawika's avatar
lintangsutawika committed
477

478
479
480
        task_name_from_string_dict = task_manager.load_task_or_group(
            string_task_name_list
        )
481

482
483
    for task_element in others_task_name_list:
        if isinstance(task_element, dict):
484
485
            task_name_from_config_dict = {
                **task_name_from_config_dict,
486
                **task_manager.load_config(config=task_element),
487
488
489
490
491
            }

        elif isinstance(task_element, Task):
            task_name_from_object_dict = {
                **task_name_from_object_dict,
lintangsutawika's avatar
lintangsutawika committed
492
                get_task_name_from_object(task_element): task_element,
493
            }
lintangsutawika's avatar
lintangsutawika committed
494

495
    if not set(task_name_from_string_dict.keys()).isdisjoint(
lintangsutawika's avatar
lintangsutawika committed
496
        set(task_name_from_object_dict.keys())
497
498
    ):
        raise ValueError
499

lintangsutawika's avatar
lintangsutawika committed
500
    return {
501
        **task_name_from_string_dict,
lintangsutawika's avatar
lintangsutawika committed
502
        **task_name_from_config_dict,
503
        **task_name_from_object_dict,
lintangsutawika's avatar
lintangsutawika committed
504
    }