Spaces:
Sleeping
Sleeping
CI: sync Gap-Finder Space (0c1e9a4)
Browse files
src/uofa_cli/adversarial/judge/resume.py
CHANGED
|
@@ -5,7 +5,9 @@ object per case. On rerun with `--resume`, we:
|
|
| 5 |
|
| 6 |
1. Read existing JSONL files for each judge to build a set of done
|
| 7 |
case_ids.
|
| 8 |
-
2. Skip cases already present in ALL active judges' files
|
|
|
|
|
|
|
| 9 |
3. Open the JSONL files in append mode so the resumed run continues
|
| 10 |
where the previous one stopped.
|
| 11 |
4. Tolerate corrupted trailing lines: a partial write at SIGTERM time
|
|
@@ -60,9 +62,14 @@ def compute_remaining_cases(
|
|
| 60 |
"""Return case_ids that still need at least one judge's verdict.
|
| 61 |
|
| 62 |
A case is 'done' only when every active judge has produced a verdict
|
| 63 |
-
for it
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
"""
|
| 67 |
if not done_per_judge:
|
| 68 |
return list(all_case_ids)
|
|
|
|
| 5 |
|
| 6 |
1. Read existing JSONL files for each judge to build a set of done
|
| 7 |
case_ids.
|
| 8 |
+
2. Skip cases already present in ALL active judges' files, and within a
|
| 9 |
+
revisited case skip the individual judges that already hold it (so an
|
| 10 |
+
already-complete judge is never re-run, and rows are never duplicated).
|
| 11 |
3. Open the JSONL files in append mode so the resumed run continues
|
| 12 |
where the previous one stopped.
|
| 13 |
4. Tolerate corrupted trailing lines: a partial write at SIGTERM time
|
|
|
|
| 62 |
"""Return case_ids that still need at least one judge's verdict.
|
| 63 |
|
| 64 |
A case is 'done' only when every active judge has produced a verdict
|
| 65 |
+
for it; a case missing from at least one judge is returned here so the
|
| 66 |
+
run revisits it.
|
| 67 |
+
|
| 68 |
+
The runner then judges only the (case, judge) pairs that are actually
|
| 69 |
+
missing — a judge already holding a verdict for the case is skipped.
|
| 70 |
+
Revisiting a case therefore neither duplicates rows nor re-spends on
|
| 71 |
+
judges that are already complete. Downstream triage aligns trios by
|
| 72 |
+
case_id, so per-judge row counts need not match.
|
| 73 |
"""
|
| 74 |
if not done_per_judge:
|
| 75 |
return list(all_case_ids)
|
src/uofa_cli/adversarial/judge/runner.py
CHANGED
|
@@ -827,24 +827,39 @@ async def _judge_bundle(
|
|
| 827 |
cases = cases[:5]
|
| 828 |
|
| 829 |
total_count = len(cases)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
# Resume gate: drop already-done cases.
|
| 831 |
if resume:
|
| 832 |
-
done_per_judge
|
| 833 |
pos: load_done_case_ids(out_dir / f"judgments_{pos}.jsonl")
|
| 834 |
for pos in positions
|
| 835 |
}
|
| 836 |
case_ids = [c.case_id for c in cases]
|
| 837 |
remaining_ids = set(compute_remaining_cases(case_ids, done_per_judge))
|
| 838 |
cases = [c for c in cases if c.case_id in remaining_ids]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
info(
|
| 840 |
f"resume: {len(cases)}/{total_count} cases remaining "
|
| 841 |
-
f"(skipped {total_count - len(cases)})"
|
|
|
|
|
|
|
| 842 |
)
|
| 843 |
|
| 844 |
-
# Collect verdicts in memory so the calibration summary doesn't
|
| 845 |
-
# to re-read partially-written JSONL files (the handles below are
|
| 846 |
-
# buffered; reading them mid-loop returns nothing).
|
| 847 |
-
|
|
|
|
|
|
|
| 848 |
|
| 849 |
# Resume mode opens in append; fresh runs truncate.
|
| 850 |
open_mode = "a" if resume else "w"
|
|
@@ -917,6 +932,10 @@ async def _judge_bundle(
|
|
| 917 |
for entry in cases:
|
| 918 |
case = _build_case(entry)
|
| 919 |
for pos, token, provider in zip(positions, tokens, providers):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
if tracker is not None and tracker.over_budget:
|
| 921 |
halted_early = True
|
| 922 |
break
|
|
@@ -928,7 +947,7 @@ async def _judge_bundle(
|
|
| 928 |
await tpm_tracker.sleep_until_authorized(token, projected)
|
| 929 |
judgment = await provider.judge(case)
|
| 930 |
handles[pos].write(json.dumps(_judgment_to_dict(judgment)) + "\n")
|
| 931 |
-
|
| 932 |
if request_tracker is not None:
|
| 933 |
request_tracker.record(token)
|
| 934 |
if tpm_tracker is not None:
|
|
@@ -969,6 +988,9 @@ async def _judge_bundle(
|
|
| 969 |
_judge_one(entry, pos, token, provider)
|
| 970 |
for entry in cases
|
| 971 |
for pos, token, provider in zip(positions, tokens, providers)
|
|
|
|
|
|
|
|
|
|
| 972 |
]
|
| 973 |
for coro in asyncio.as_completed(tasks):
|
| 974 |
pos, _case_id, judgment = await coro
|
|
@@ -976,7 +998,7 @@ async def _judge_bundle(
|
|
| 976 |
continue
|
| 977 |
handles[pos].write(json.dumps(_judgment_to_dict(judgment)) + "\n")
|
| 978 |
handles[pos].flush()
|
| 979 |
-
|
| 980 |
if tracker is not None and tracker.over_budget:
|
| 981 |
halted_early = True
|
| 982 |
finally:
|
|
@@ -1028,19 +1050,45 @@ async def _judge_bundle(
|
|
| 1028 |
"correct_count": cal_result.correct_count,
|
| 1029 |
}, indent=2))
|
| 1030 |
|
| 1031 |
-
|
| 1032 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
)
|
| 1034 |
# JSON doesn't have a NaN literal; substitute null (matches §12.2 convention).
|
| 1035 |
def _nan_to_none(x: float) -> float | None:
|
| 1036 |
return None if isinstance(x, float) and x != x else x
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
|
| 1045 |
|
| 1046 |
def _judgment_to_dict(j: Judgment) -> dict[str, Any]:
|
|
|
|
| 827 |
cases = cases[:5]
|
| 828 |
|
| 829 |
total_count = len(cases)
|
| 830 |
+
# Per-judge done sets: empty on a fresh run, populated by --resume. A judge
|
| 831 |
+
# that already holds a verdict for a case is skipped at dispatch below, so
|
| 832 |
+
# revisiting a case never duplicates its row or re-spends on a judge that
|
| 833 |
+
# is already complete.
|
| 834 |
+
done_per_judge: dict[str, set[str]] = {pos: set() for pos in positions}
|
| 835 |
# Resume gate: drop already-done cases.
|
| 836 |
if resume:
|
| 837 |
+
done_per_judge = {
|
| 838 |
pos: load_done_case_ids(out_dir / f"judgments_{pos}.jsonl")
|
| 839 |
for pos in positions
|
| 840 |
}
|
| 841 |
case_ids = [c.case_id for c in cases]
|
| 842 |
remaining_ids = set(compute_remaining_cases(case_ids, done_per_judge))
|
| 843 |
cases = [c for c in cases if c.case_id in remaining_ids]
|
| 844 |
+
pending_calls = sum(
|
| 845 |
+
1
|
| 846 |
+
for c in cases
|
| 847 |
+
for pos in positions
|
| 848 |
+
if c.case_id not in done_per_judge[pos]
|
| 849 |
+
)
|
| 850 |
info(
|
| 851 |
f"resume: {len(cases)}/{total_count} cases remaining "
|
| 852 |
+
f"(skipped {total_count - len(cases)}); "
|
| 853 |
+
f"{pending_calls} judge-calls pending "
|
| 854 |
+
f"({len(cases) * len(positions) - pending_calls} already-judged pairs skipped)"
|
| 855 |
)
|
| 856 |
|
| 857 |
+
# Collect this run's verdicts in memory so the calibration summary doesn't
|
| 858 |
+
# have to re-read partially-written JSONL files (the handles below are
|
| 859 |
+
# buffered; reading them mid-loop returns nothing). Keyed by case_id rather
|
| 860 |
+
# than append order: the concurrent path completes out of order, and
|
| 861 |
+
# --resume can skip a judge for a case, so the per-judge sets need not match.
|
| 862 |
+
verdicts_by_case: dict[str, dict[str, str]] = {pos: {} for pos in positions}
|
| 863 |
|
| 864 |
# Resume mode opens in append; fresh runs truncate.
|
| 865 |
open_mode = "a" if resume else "w"
|
|
|
|
| 932 |
for entry in cases:
|
| 933 |
case = _build_case(entry)
|
| 934 |
for pos, token, provider in zip(positions, tokens, providers):
|
| 935 |
+
# This judge already holds a verdict for this case (resume):
|
| 936 |
+
# re-judging would duplicate the row and re-spend.
|
| 937 |
+
if entry.case_id in done_per_judge[pos]:
|
| 938 |
+
continue
|
| 939 |
if tracker is not None and tracker.over_budget:
|
| 940 |
halted_early = True
|
| 941 |
break
|
|
|
|
| 947 |
await tpm_tracker.sleep_until_authorized(token, projected)
|
| 948 |
judgment = await provider.judge(case)
|
| 949 |
handles[pos].write(json.dumps(_judgment_to_dict(judgment)) + "\n")
|
| 950 |
+
verdicts_by_case[pos][judgment.case_id] = judgment.verdict
|
| 951 |
if request_tracker is not None:
|
| 952 |
request_tracker.record(token)
|
| 953 |
if tpm_tracker is not None:
|
|
|
|
| 988 |
_judge_one(entry, pos, token, provider)
|
| 989 |
for entry in cases
|
| 990 |
for pos, token, provider in zip(positions, tokens, providers)
|
| 991 |
+
# Skip (case, judge) pairs this judge already holds (resume):
|
| 992 |
+
# re-judging would duplicate the row and re-spend.
|
| 993 |
+
if entry.case_id not in done_per_judge[pos]
|
| 994 |
]
|
| 995 |
for coro in asyncio.as_completed(tasks):
|
| 996 |
pos, _case_id, judgment = await coro
|
|
|
|
| 998 |
continue
|
| 999 |
handles[pos].write(json.dumps(_judgment_to_dict(judgment)) + "\n")
|
| 1000 |
handles[pos].flush()
|
| 1001 |
+
verdicts_by_case[pos][judgment.case_id] = judgment.verdict
|
| 1002 |
if tracker is not None and tracker.over_budget:
|
| 1003 |
halted_early = True
|
| 1004 |
finally:
|
|
|
|
| 1050 |
"correct_count": cal_result.correct_count,
|
| 1051 |
}, indent=2))
|
| 1052 |
|
| 1053 |
+
# Align by case_id over the cases all three judged in THIS run. With
|
| 1054 |
+
# --resume, a judge already holding a case is skipped, so the per-judge
|
| 1055 |
+
# verdict sets can differ; index-alignment would silently pair up
|
| 1056 |
+
# different cases (the concurrent path also completes out of order).
|
| 1057 |
+
common = sorted(
|
| 1058 |
+
set(verdicts_by_case["A"])
|
| 1059 |
+
& set(verdicts_by_case["B"])
|
| 1060 |
+
& set(verdicts_by_case["C"])
|
| 1061 |
)
|
| 1062 |
# JSON doesn't have a NaN literal; substitute null (matches §12.2 convention).
|
| 1063 |
def _nan_to_none(x: float) -> float | None:
|
| 1064 |
return None if isinstance(x, float) and x != x else x
|
| 1065 |
+
if common:
|
| 1066 |
+
stats = compute_agreement(
|
| 1067 |
+
[verdicts_by_case["A"][c] for c in common],
|
| 1068 |
+
[verdicts_by_case["B"][c] for c in common],
|
| 1069 |
+
[verdicts_by_case["C"][c] for c in common],
|
| 1070 |
+
)
|
| 1071 |
+
summary = {
|
| 1072 |
+
"case_count": stats.case_count,
|
| 1073 |
+
"pairwise_kappa_AB": _nan_to_none(stats.cohen_kappa_AB),
|
| 1074 |
+
"pairwise_kappa_AC": _nan_to_none(stats.cohen_kappa_AC),
|
| 1075 |
+
"pairwise_kappa_BC": _nan_to_none(stats.cohen_kappa_BC),
|
| 1076 |
+
"fleiss_kappa": _nan_to_none(stats.fleiss_kappa),
|
| 1077 |
+
}
|
| 1078 |
+
else:
|
| 1079 |
+
# No case was judged by all three this run (e.g. a resume that only
|
| 1080 |
+
# topped up one judge). Corpus-wide agreement comes from
|
| 1081 |
+
# `uofa adversarial adjudicate`, which aligns the JSONL by case_id.
|
| 1082 |
+
summary = {
|
| 1083 |
+
"case_count": 0,
|
| 1084 |
+
"pairwise_kappa_AB": None,
|
| 1085 |
+
"pairwise_kappa_AC": None,
|
| 1086 |
+
"pairwise_kappa_BC": None,
|
| 1087 |
+
"fleiss_kappa": None,
|
| 1088 |
+
}
|
| 1089 |
+
(out_dir / "calibration_results_summary.json").write_text(
|
| 1090 |
+
json.dumps(summary, indent=2)
|
| 1091 |
+
)
|
| 1092 |
|
| 1093 |
|
| 1094 |
def _judgment_to_dict(j: Judgment) -> dict[str, Any]:
|