system HF Staff commited on
Commit
64f8981
·
verified ·
1 Parent(s): b4465b2

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. Any case missing from any judge is re-judged across ALL
64
- judges so the per-case row count stays consistent (downstream triage
65
- requires aligned trios).
 
 
 
 
 
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: dict[str, set[str]] = {
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 have
845
- # to re-read partially-written JSONL files (the handles below are
846
- # buffered; reading them mid-loop returns nothing).
847
- verdicts_by_pos: dict[str, list[str]] = {pos: [] for pos in positions}
 
 
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
- verdicts_by_pos[pos].append(judgment.verdict)
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
- verdicts_by_pos[pos].append(judgment.verdict)
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
- stats = compute_agreement(
1032
- verdicts_by_pos["A"], verdicts_by_pos["B"], verdicts_by_pos["C"]
 
 
 
 
 
 
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
- (out_dir / "calibration_results_summary.json").write_text(json.dumps({
1038
- "case_count": stats.case_count,
1039
- "pairwise_kappa_AB": _nan_to_none(stats.cohen_kappa_AB),
1040
- "pairwise_kappa_AC": _nan_to_none(stats.cohen_kappa_AC),
1041
- "pairwise_kappa_BC": _nan_to_none(stats.cohen_kappa_BC),
1042
- "fleiss_kappa": _nan_to_none(stats.fleiss_kappa),
1043
- }, indent=2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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]: