Move validator business logic into every_eval_ever and update to de-duplication logic#194
Move validator business logic into every_eval_ever and update to de-duplication logic#194nelaturuharsha wants to merge 2 commits into
Conversation
Erotemic
left a comment
There was a problem hiding this comment.
Mostly just a python-level review, I didn't consider anything holistically.
| file_fingerprints: dict[str, str] = {} | ||
| warnings: list[str] = [] | ||
| for file_path in sorted(file_paths): | ||
| if not file_path.endswith('.json'): |
There was a problem hiding this comment.
This is a footgun. If you are expecting this to be called with only json paths, and you ever hit this case, you are silently dropping information about some bad pre-filtering done when constructing the content passed to this function. I would either remove this continue or raise an error.
There was a problem hiding this comment.
I had this in place because we don't have a good recipe to reliably de-duplicate jsonls at the moment and we have only jsons or jsonls in the target datastore.
| def compute_fingerprint(content: bytes) -> str: | ||
| """Compute the semantic duplicate fingerprint for aggregate JSON bytes.""" | ||
| try: | ||
| data = json.loads(content) |
There was a problem hiding this comment.
This might end up being a bottleneck. It may be worth it to abstract the specific json reader / writer so the stdlib json is always an option, but you can opt-in to something like orjson or ujson. (Note: orjson is a bit opinionated. It's fast, but it also means you can't serialize generic floats like nans or infs, which technically isn't json, but the stdlib and ujson aren't so strict about it).
I've handled this in a few ways in the past: simple wrappers: https://gitlab.kitware.com/computer-vision/kwutil/-/blob/main/kwutil/util_json.py?blame=1&ref_type=heads#L391
And module level globals: https://gitlab.kitware.com/computer-vision/kwcoco/-/blob/main/kwcoco/coco_dataset.py?blame=1&ref_type=heads#L117
| return value | ||
|
|
||
|
|
||
| def _canon(obj: Any) -> Any: |
There was a problem hiding this comment.
The recursive implementation could probably be much faster using an iterator and maintaining your own stack: https://gitlab.kitware.com/computer-vision/kwutil/-/blob/main/kwutil/util_json.py?ref_type=heads#L29
A utility I wrote makes this fairly easy to express without too much extra complication: https://github.com/Erotemic/ubelt/blob/main/ubelt/util_indexable.py#L52
There was a problem hiding this comment.
Think we're just going fingerprints that we created and not the JSON, would this still be a big performance gap you think?
There was a problem hiding this comment.
I'd have to profile it to know, but recursion can get pretty costly. I just tend to avoid it when writing new code. Might be fine the way it is.
FWIW: a quick pass with sample data on both variants with line-profiler will tell you a lot.
| .sass-cache/ | ||
| .jekyll-cache/ | ||
| .jekyll-metadata No newline at end of file | ||
| .jekyll-metadataUSAGE_* |
| local_paths = {file_path: file_path for file_path in file_paths} | ||
| dedup_report = check_duplicates(file_paths, local_paths, {'files': {}}) | ||
| duplicate_results = [ | ||
| result for result in dedup_report.results if result.duplicate_of | ||
| ] |
There was a problem hiding this comment.
This looks like it only does local same-batch duplicate detection now. Since the manifest is always {'files': {}}, this path can’t catch “this candidate already exists in the accepted datastore,” even though the docs say the PR bot compares candidates against accepted records. Could we either load/pass the real manifest here, or make this command/docs explicit that this is local-only? Also, since file_paths are local CLI paths, we probably want to normalize them through repo_path_from_path; otherwise ./data/... or absolute paths won’t scope correctly by data/.
| def _source_identity(source_data: Any) -> dict[str, Any]: | ||
| if not isinstance(source_data, dict): | ||
| return {} | ||
| identity = { | ||
| 'type': _norm_str(source_data.get('source_type')), | ||
| 'name': _norm_str(source_data.get('dataset_name')), | ||
| } | ||
| if source_data.get('source_type') == 'hf_dataset': | ||
| repo = _norm_str(source_data.get('hf_repo')) | ||
| if repo: | ||
| identity['hf'] = repo | ||
| return identity |
There was a problem hiding this comment.
This seems too loose for non-HF sources. For url or leaderboard-style sources, two different datasets with the same dataset_name would collapse to the same source identity because the URL/source artifact/version is ignored. That could create false duplicate hits across different leaderboards or dataset releases. Could we include a normalized URL / source artifact / version field when present, not just dataset_name?
| def check_companion_exists( | ||
| repo_path: str, | ||
| aggregate_data: dict[str, Any], | ||
| available_files: Container[str], | ||
| ) -> list[str]: | ||
| """Warn when aggregate detailed results point to a missing JSONL companion.""" | ||
| detail = aggregate_data.get('detailed_evaluation_results') | ||
| if not isinstance(detail, dict) or not detail.get('file_path'): | ||
| return [] | ||
|
|
||
| folder = Path(repo_path).parent | ||
| uuid = Path(repo_path).stem | ||
| expected = { | ||
| str(folder / f'{uuid}.jsonl'), | ||
| str(folder / f'{uuid}_samples.jsonl'), | ||
| } | ||
| if not any(path in available_files for path in expected): | ||
| return [ | ||
| f"Companion .jsonl for '{Path(repo_path).name}' not found " | ||
| 'in the dataset or this PR' | ||
| ] | ||
| return [] |
There was a problem hiding this comment.
This ignores the actual detailed_evaluation_results.file_path and just infers the companion path from the aggregate UUID. That can pass when the referenced file is wrong but a same-UUID JSONL exists, and fail when the aggregate intentionally points to a different relative path. Could we validate the referenced path directly, maybe with same-folder UUID aliases as a fallback?
| for index, result in enumerate(results): | ||
| if not isinstance(result, dict): | ||
| continue | ||
| metric = result.get('metric_config') | ||
| if not isinstance(metric, dict): | ||
| continue | ||
| for key in ('score_type', 'min_score', 'max_score'): | ||
| if key not in metric: | ||
| warnings.append( | ||
| f"evaluation_results[{index}].metric_config: missing '{key}'" | ||
| ) | ||
|
|
||
| score_details = result.get('score_details') | ||
| if not isinstance(score_details, dict): | ||
| continue | ||
| score = score_details.get('score') | ||
| lo = metric.get('min_score') | ||
| hi = metric.get('max_score') | ||
| if ( | ||
| isinstance(score, (int, float)) | ||
| and not isinstance(score, bool) | ||
| and isinstance(lo, (int, float)) | ||
| and not isinstance(lo, bool) | ||
| and isinstance(hi, (int, float)) | ||
| and not isinstance(hi, bool) | ||
| and (score < lo or score > hi) | ||
| ): | ||
| warnings.append( | ||
| f'evaluation_results[{index}]: score {score} is outside ' | ||
| f'[min_score={lo}, max_score={hi}]' | ||
| ) |
There was a problem hiding this comment.
This only warns when the key is absent, but fails silently when they are present but null or NaN, and the bound checks for lo and hi are also silently skipped. If these fields are now expected, can we treat None/blank the same as missing?
Ruff/Ty were run which formatted some adapters.