Skip to content

Commit 5bd8fbc

Browse files
authored
fix: core evaluator/cache/utils correctness fixes (#1378)
* fix: core evaluator/cache/utils correctness fixes - cache.save_to_cache: accumulate serializable_obj per item (previously mutated subitems but never appended, so it always pickled an empty list) - response_cache: recursively fingerprint nested task groups so subtasks get real fingerprints instead of falling back to empty strings - evaluator: - redact api_key/token/hf_token-style values from results.config - clone padding Instances so [pad_source]*n no longer aliases one object - auto-init a gloo process group and inject lm._rank/_world_size for multi-rank launches whose backend never called init_process_group - fix torchrun all_gather_into_tensor (1-D input tensor, world_size buffer) - defensively skip docs with no requests in the postprocess loop - utils: - repo-root-anchored git commit/branch helpers with env fallbacks (LMMS_EVAL_GIT_COMMIT/GITHUB_SHA/GITHUB_REF_NAME) and detached-HEAD handling - build_eval_output_dir helper - resolve relative dataset_kwargs.data_files to absolute paths for local csv/json/parquet builders - py3.13 re.split(maxsplit=) keyword fix * fix(evaluator): gate gloo auto-init to the non-accelerate path; isolate padding clones per slot
1 parent 3fc9fa8 commit 5bd8fbc

4 files changed

Lines changed: 198 additions & 26 deletions

File tree

lmms_eval/caching/cache.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,13 @@ def save_to_cache(file_name, obj):
4444
serializable_obj = []
4545

4646
for item in obj:
47+
serializable_item = []
4748
for subitem in item:
4849
if hasattr(subitem, "arguments"): # we need to handle the arguments specially since doc_to_visual is callable method and not serializable
4950
serializable_arguments = tuple(arg if not callable(arg) else None for arg in subitem.arguments)
5051
subitem.arguments = serializable_arguments
52+
serializable_item.append(subitem)
53+
serializable_obj.append(serializable_item)
5154

5255
eval_logger.debug(f"Saving {file_path} to cache...")
5356
try:

lmms_eval/caching/response_cache.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,21 @@ def create(
407407
cache_root = os.path.dirname(os.path.abspath(cache_root))
408408
cache_root = os.path.abspath(cache_root)
409409

410-
# Task fingerprints
410+
# Task fingerprints — recursively process nested groups so subtasks
411+
# get proper fingerprints instead of falling back to empty strings.
411412
task_fingerprints = {}
412413
if task_dict:
413-
for tname, tobj in task_dict.items():
414-
if hasattr(tobj, "dump_config"):
415-
cfg_str = json.dumps(tobj.dump_config(), sort_keys=True, default=str)
416-
cfg_str = _FUNC_ADDR_RE.sub(">", cfg_str)
417-
task_fingerprints[tname] = hash_string(cfg_str)[:16]
414+
415+
def _collect_fingerprints(d):
416+
for tname, tobj in d.items():
417+
if hasattr(tobj, "dump_config"):
418+
cfg_str = json.dumps(tobj.dump_config(), sort_keys=True, default=str)
419+
cfg_str = _FUNC_ADDR_RE.sub(">", cfg_str)
420+
task_fingerprints[tname] = hash_string(cfg_str)[:16]
421+
elif isinstance(tobj, dict):
422+
_collect_fingerprints(tobj)
423+
424+
_collect_fingerprints(task_dict)
418425

419426
# Model fingerprint
420427
if isinstance(model_args, dict):

lmms_eval/evaluator.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@
7373
)
7474

7575
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff")
76+
_SENSITIVE_CONFIG_KEYS = {
77+
"api_key",
78+
"client_secret",
79+
"hf_token",
80+
"huggingface_hub_token",
81+
"huggingfacehub_api_token",
82+
"token",
83+
}
84+
# Suffixes also catch prefixed variants: openai_api_key, github_token, my_secret, db_password.
85+
_SENSITIVE_KEY_SUFFIXES = ("api_key", "_token", "_secret", "password")
86+
_SECRET_ASSIGNMENT_RE = re.compile(r"(?i)(^|[,\s])(\w*(?:api_key|token|secret|password))=([^,\s]+)")
87+
_HF_TOKEN_VALUE_RE = re.compile(r"\bhf_[A-Za-z0-9]{20,}\b")
88+
89+
90+
def _is_sensitive_config_key(key) -> bool:
91+
key_lower = str(key).lower()
92+
return key_lower in _SENSITIVE_CONFIG_KEYS or key_lower.endswith(_SENSITIVE_KEY_SUFFIXES)
93+
94+
95+
def _redact_eval_config_secrets(value):
96+
if isinstance(value, dict):
97+
return {key: ("[REDACTED]" if _is_sensitive_config_key(key) else _redact_eval_config_secrets(item)) for key, item in value.items()}
98+
if isinstance(value, (list, tuple)):
99+
return type(value)(_redact_eval_config_secrets(item) for item in value)
100+
if isinstance(value, str):
101+
value = _SECRET_ASSIGNMENT_RE.sub(r"\1\2=[REDACTED]", value)
102+
return _HF_TOKEN_VALUE_RE.sub("[REDACTED]", value)
103+
return value
104+
105+
106+
def _clone_padding_request(pad_source: Instance) -> Instance:
107+
pad_instance = copy.copy(pad_source)
108+
pad_instance.metadata = dict(pad_source.metadata or {})
109+
pad_instance.metadata["__padding_only__"] = True
110+
pad_instance.resps = []
111+
pad_instance.token_counts = []
112+
return pad_instance
76113

77114

78115
def _enable_reentrant_filelocks() -> None:
@@ -531,6 +568,7 @@ def _adjust_config(task_dict):
531568
# add info about execution
532569
results["config"].update(
533570
{
571+
"model_backend": f"{type(lm).__module__}.{type(lm).__name__} ({task_type})",
534572
"batch_size": batch_size,
535573
"batch_sizes": (list(lm.batch_sizes.values()) if hasattr(lm, "batch_sizes") else []),
536574
"device": device,
@@ -556,6 +594,8 @@ def _adjust_config(task_dict):
556594
resolved[key] = str(value)
557595
results["config"]["resolved_cli_args"] = resolved
558596

597+
results["config"] = _redact_eval_config_secrets(results["config"])
598+
559599
results["git_hash"] = get_git_commit_hash()
560600
results["git_branch"] = get_git_branch_name()
561601
results["lmms_eval_version"] = get_lmms_eval_version_string()
@@ -862,6 +902,7 @@ def evaluate(
862902
local_rank = int(os.environ.get("LOCAL_RANK", 0))
863903
global_rank = int(os.environ.get("RANK", 0))
864904
world_size = int(os.environ.get("WORLD_SIZE", 1))
905+
865906
eval_logger.info(f"Running on rank {global_rank} (local rank {local_rank})")
866907

867908
def _infer_task_request_type(task_obj: Task) -> Optional[str]:
@@ -882,8 +923,32 @@ def _infer_task_request_type(task_obj: Task) -> Optional[str]:
882923
if not all("bypass" not in getattr(task_output.task, "_metric_fn_list", {}).keys() for task_output in eval_tasks):
883924
raise ValueError("log_samples must be True for 'bypass' metric-only tasks")
884925

885-
if distributed_executor_backend == "accelerate" and not hasattr(lm, "accelerator"):
886-
lm.accelerator = Accelerator()
926+
if distributed_executor_backend == "accelerate":
927+
if not hasattr(lm, "accelerator"):
928+
lm.accelerator = Accelerator()
929+
else:
930+
# Torchrun/native path: auto-init a process group for multi-rank runs
931+
# whose model backend never called init_process_group (e.g. diffusers
932+
# video-gen models), so evaluator collectives don't crash. gloo needs no
933+
# GPU. Gated out of the accelerate path on purpose: pre-initializing here
934+
# would make Accelerator() adopt this gloo group instead of building NCCL,
935+
# silently running GPU collectives on CPU.
936+
if world_size > 1 and dist.is_available() and not dist.is_initialized():
937+
if os.environ.get("MASTER_ADDR") and os.environ.get("MASTER_PORT"):
938+
dist.init_process_group(backend="gloo")
939+
eval_logger.info(f"evaluator: auto-initialized gloo process group (world={world_size})")
940+
else:
941+
eval_logger.warning(
942+
f"evaluator: WORLD_SIZE={world_size} but MASTER_ADDR/MASTER_PORT are unset; skipping torch.distributed auto-init. Distributed collectives will fail unless the model backend initializes the process group itself."
943+
)
944+
945+
# Inject rank/world_size into the model so that logging (tqdm disable)
946+
# and rank-conditional logic work on non-zero ranks. Many simple models
947+
# only read LOCAL_RANK for device binding and leave _rank/_world_size
948+
# at their defaults (0, 1).
949+
if world_size > 1:
950+
lm._rank = global_rank
951+
lm._world_size = world_size
887952

888953
for task_output in eval_tasks:
889954
task = task_output.task
@@ -950,8 +1015,8 @@ def _infer_task_request_type(task_obj: Task) -> Optional[str]:
9501015
instances_rnk = torch.tensor(len(task._instances), device=lm.device)
9511016
gathered_item = lm.accelerator.gather(instances_rnk).cpu().detach().numpy().tolist()
9521017
elif distributed_executor_backend == "torchrun":
953-
instances_rnk = torch.tensor(len(task._instances), device=lm.device)
954-
gathered_item = torch.zeros(world_size * 1, dtype=instances_rnk.dtype, device=lm.device)
1018+
instances_rnk = torch.tensor([len(task._instances)], device=lm.device)
1019+
gathered_item = torch.zeros(world_size, dtype=instances_rnk.dtype, device=lm.device)
9551020
dist.all_gather_into_tensor(gathered_item, instances_rnk)
9561021
gathered_item = gathered_item.cpu().detach().numpy().tolist()
9571022
else:
@@ -1003,7 +1068,8 @@ def _infer_task_request_type(task_obj: Task) -> Optional[str]:
10031068
eval_logger.warning(f"Running {reqtype} requests but could not find a pad source request on rank {global_rank}; skipping rank padding.")
10041069
else:
10051070
for _ in range(padding_requests[reqtype]):
1006-
cloned_reqs.extend([pad_source] * pad_source.repeats)
1071+
pad_instance = _clone_padding_request(pad_source)
1072+
cloned_reqs.extend([pad_instance] * pad_instance.repeats)
10071073

10081074
# run requests through model (with optional response cache)
10091075
if reqtype == "generate_until_agentic":
@@ -1124,6 +1190,12 @@ def _infer_task_request_type(task_obj: Task) -> Optional[str]:
11241190
pbar = tqdm(total=total_docs, desc="Postprocessing", disable=(RANK != 0))
11251191
for doc_id, doc in doc_iterator:
11261192
requests = instances_by_doc_id[doc_id]
1193+
# Defensive skip: if a doc landed in this rank's shard but has
1194+
# no requests (e.g. a split mismatch during a partial rerun),
1195+
# skip rather than crashing at `requests[0].doc` below.
1196+
if not requests:
1197+
pbar.update(1)
1198+
continue
11271199

11281200
# Strip reasoning tags before scoring
11291201
if reasoning_tags is not None:

lmms_eval/utils.py

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def escaped_split(text, sep_char, maxsplit=-1):
7676
return text
7777
maxsplit = max(0, maxsplit)
7878

79-
return re.split(r"(?<!\\)" + sep_char, text, maxsplit)
79+
return re.split(r"(?<!\\)" + sep_char, text, maxsplit=maxsplit)
8080

8181

8282
def handle_arg_string(arg):
@@ -775,27 +775,65 @@ def run_task_tests(task_list: List[str]):
775775
raise ValueError(f"Not all tests for the specified tasks ({task_list}) ran successfully! Error code: {pytest_return_val}")
776776

777777

778+
def _lmms_eval_repo_root() -> pathlib.Path:
779+
return pathlib.Path(__file__).resolve().parents[1]
780+
781+
782+
def _git_output(*args: str) -> str:
783+
return (
784+
subprocess.check_output(
785+
["git", "-C", str(_lmms_eval_repo_root()), *args],
786+
stderr=subprocess.DEVNULL,
787+
)
788+
.strip()
789+
.decode()
790+
)
791+
792+
793+
def _first_env(*names: str) -> str | None:
794+
for name in names:
795+
value = os.environ.get(name, "").strip()
796+
if value:
797+
return value
798+
return None
799+
800+
801+
def _normalize_branch_name(branch: str) -> str:
802+
return branch.removeprefix("remotes/origin/")
803+
804+
805+
def _is_exact_git_ref_name(branch: str) -> bool:
806+
return bool(branch and branch != "undefined" and "^" not in branch and "~" not in branch)
807+
808+
778809
def get_git_commit_hash():
779-
"""
780-
Gets the git commit hash of your current repo (if it exists).
781-
Source: https://github.com/EleutherAI/gpt-neox/blob/b608043be541602170bfcfb8ec9bf85e8a0799e0/megatron/neox_arguments/neox_args.py#L42
782-
"""
783810
try:
784-
git_hash = subprocess.check_output(["git", "describe", "--always"]).strip()
785-
git_hash = git_hash.decode()
811+
git_hash = _git_output("describe", "--always")
786812
except (subprocess.CalledProcessError, FileNotFoundError):
787-
# FileNotFoundError occurs when git not installed on system
788-
git_hash = None
813+
git_hash = _first_env("LMMS_EVAL_GIT_COMMIT", "GIT_COMMIT", "GITHUB_SHA")
789814
return git_hash
790815

791816

792817
def get_git_branch_name():
793-
"""Gets the current git branch name (if in a repo)."""
794818
try:
795-
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip()
796-
return branch.decode()
819+
branch = _git_output("branch", "--show-current")
820+
if branch:
821+
return _normalize_branch_name(branch)
822+
except (subprocess.CalledProcessError, FileNotFoundError):
823+
pass
824+
825+
branch = _first_env("LMMS_EVAL_GIT_BRANCH", "GIT_BRANCH", "BRANCH_NAME", "GITHUB_REF_NAME")
826+
if branch:
827+
return _normalize_branch_name(branch)
828+
829+
try:
830+
branch = _git_output("name-rev", "--name-only", "--exclude=tags/*", "HEAD")
831+
if _is_exact_git_ref_name(branch):
832+
return _normalize_branch_name(branch)
797833
except (subprocess.CalledProcessError, FileNotFoundError):
798-
return None
834+
pass
835+
836+
return "detached" if get_git_commit_hash() else None
799837

800838

801839
def get_lmms_eval_version_string():
@@ -919,6 +957,58 @@ def import_function(loader, node):
919957
raise ImportError(f"Failed to import function '{function_name}' from module '{module_name}'. " f"Tried relative path '{module_path}' and absolute import.") from ex
920958

921959

960+
_LOCAL_BUILDERS = frozenset({"csv", "json", "parquet", "text", "pandas", "arrow"})
961+
962+
963+
def _looks_like_url(value: str) -> bool:
964+
return value.startswith(("http://", "https://", "s3://", "gs://", "ftp://", "ftps://"))
965+
966+
967+
def _resolve_local_data_files(value, yaml_dir: str):
968+
"""Rewrite relative ``data_files`` paths to absolute paths anchored at ``yaml_dir``.
969+
970+
Conservative: only rewrites a string when it's a relative path AND the
971+
resolved absolute path exists on disk. Leaves absolute paths, URLs, and
972+
non-existent relatives untouched. Recurses into dict/list to handle the
973+
``data_files: {train: <path>, validation: <path>}`` and ``data_files:
974+
[<path>, <path>]`` shapes that ``datasets.load_dataset`` accepts.
975+
"""
976+
if isinstance(value, str):
977+
if not value or _looks_like_url(value) or os.path.isabs(value):
978+
return value
979+
candidate = os.path.join(yaml_dir, value)
980+
if os.path.isfile(candidate):
981+
return candidate
982+
return value
983+
if isinstance(value, dict):
984+
return {k: _resolve_local_data_files(v, yaml_dir) for k, v in value.items()}
985+
if isinstance(value, list):
986+
return [_resolve_local_data_files(v, yaml_dir) for v in value]
987+
return value
988+
989+
990+
def _maybe_resolve_yaml_local_data_files(yaml_config: dict, yaml_dir: str) -> dict:
991+
"""Resolve ``dataset_kwargs.data_files`` relative paths against ``yaml_dir``.
992+
993+
Only applied for the generic local builders (``csv``, ``json``,
994+
``parquet``, ``text``, ``pandas``, ``arrow``). For HF hub dataset
995+
paths (e.g. ``lmms-lab-eval/ssv2``) ``data_files`` is left as-is to
996+
preserve the upstream behavior of letting HF resolve relative paths
997+
against the dataset's hub root.
998+
"""
999+
dataset_path = yaml_config.get("dataset_path")
1000+
if not isinstance(dataset_path, str) or dataset_path not in _LOCAL_BUILDERS:
1001+
return yaml_config
1002+
dataset_kwargs = yaml_config.get("dataset_kwargs")
1003+
if not isinstance(dataset_kwargs, dict) or "data_files" not in dataset_kwargs:
1004+
return yaml_config
1005+
resolved = _resolve_local_data_files(dataset_kwargs["data_files"], yaml_dir)
1006+
if resolved is dataset_kwargs["data_files"]:
1007+
return yaml_config
1008+
new_kwargs = {**dataset_kwargs, "data_files": resolved}
1009+
return {**yaml_config, "dataset_kwargs": new_kwargs}
1010+
1011+
9221012
def load_yaml_config(yaml_path=None, yaml_config=None, yaml_dir=None, mode="full"):
9231013
if mode == "simple":
9241014
constructor_fn = ignore_constructor
@@ -962,8 +1052,8 @@ def load_yaml_config(yaml_path=None, yaml_config=None, yaml_dir=None, mode="full
9621052
raise ex
9631053

9641054
final_yaml_config.update(yaml_config)
965-
return final_yaml_config
966-
return yaml_config
1055+
return _maybe_resolve_yaml_local_data_files(final_yaml_config, yaml_dir)
1056+
return _maybe_resolve_yaml_local_data_files(yaml_config, yaml_dir)
9671057

9681058

9691059
def regex_replace(string, pattern, repl, count: int = 0):

0 commit comments

Comments
 (0)