Skip to content

Support native Nemotron abstention QA rewards in MILES - #2252

Open
no0p wants to merge 4 commits into
mainfrom
codex/nemotron-qa-abstention-v1
Open

no0p wants to merge 4 commits into
mainfrom
codex/nemotron-qa-abstention-v1

Conversation

@no0p

@no0p no0p commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Nemotron-RL-QA-Abstention-v1 asks a policy to answer factoid questions or abstain when uncertain. Its factual references alone are insufficient for training: ordinary answer matching gives the same score to abstention and an incorrect answer. This change adds its native source adapter and NVIDIA NeMo Gym's three-tier judged reward so RL Data Workbench can prepare the source for a future dataset release.

The nvidia/qa-abstention-v1 named rubric preserves the official OMNISCIENCE_GRADER template, thinking stripping, nested/last-box extraction, and A/B/C parsing from Gym cf7f71a57dde0a69b14a2e919b60a01b17cc7abd. It fixes the native default reward scale at correct=1, abstain=0.5, incorrect=0. Explicit normalized IDK avoids a judge request; service failures and empty replies remain ungraded. Profile/target bindings are validated before use, and the service probe checks both explicit and judge-recognized abstention as well as correct and incorrect answers.

The adapter preserves the original system/user prompt and emits question/reference targets, keeping reward code in Open Instruct. Documentation records the native parser's permissive fallback and the need for independent judge calibration. No generic-quality rubric or exact-match reward is substituted.

Validation: 219 CPU tests passed, 5 optional tests skipped across abstention, source preparation, named judging, judge failure handling and shared grading. Repository-wide Ruff lint/format and diff whitespace checks pass. Live model judging and GPU training have not run; they remain release qualification work. Workbench also needs to resolve the dataset card/row license discrepancy and run combined protected-source hygiene.

Review follow-ups preserve the existing judge failure policy: training excludes judge-error responses, while evaluation can record a diagnostic fallback zero and continue. Explicit bridge controls cover both paths.

The documented default reuses the existing judges.general Qwen/Qwen3-32B service, adding a rubric/binding without allocating another model instance. Judge-call diagnostics retain the existing general_judge discriminator for rollout error metrics; explicit IDK shortcuts are separate and do not dilute the request error rate. Live calibration is deferred per Robert.

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Changes Detected

📄 miles/source-preparation/index.html
--- site-base/miles/source-preparation/index.html	2026-11-13 20:12:19.532059422 +0000
+++ site-pr/miles/source-preparation/index.html	2026-11-13 20:12:14.218681482 +0000
@@ -1946,6 +1946,17 @@
   
 </li>
       
+        <li class="md-nav__item">
+  <a href="#abstention-aware-factoid-qa" class="md-nav__link">
+    <span class="md-ellipsis">
+      
📄 sitemap.xml
--- site-base/sitemap.xml	2026-11-13 20:12:19.533051826 +0000
+++ site-pr/sitemap.xml	2026-11-13 20:12:14.219640544 +0000
@@ -109,6 +109,10 @@
          <lastmod>2026-11-13</lastmod>
     </url>
     <url>
+         <loc>https://github.com/allenai/open-instruct/miles/abstention/</loc>
+         <lastmod>2026-11-13</lastmod>
+    </url>
+    <url>
📄 sitemap.xml.gz
Binary files site-base/sitemap.xml.gz and site-pr/sitemap.xml.gz differ

Showing first 10 lines of diff for each changed file (up to 5 files, excluding search indices).

github-actions[bot]
github-actions Bot previously approved these changes Nov 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Codex.

Reviewed commit 4614f9ffa32b70cd7048cb0a8c271d29ab1c0f6e.

No blocking findings or significant correctness regressions found.

Reviewed the exact base-to-head diff, source adaptation, rubric/target binding validation, shared grading gates, judge failure propagation, and effects on existing judge profiles. No previous Codex change requests required reassessment.

Validation: all changed Python files compile; standalone checks of the head’s parser, reward tiers, empty-reply handling, and pinned rubric hash passed. Diff whitespace checks passed. Pytest and Ruff are unavailable in this sandbox. Live judge calibration and GPU training remain unverified, as disclosed in the PR; no GPU validation run was claimed.

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Changes Detected

📄 miles/source-preparation/index.html
--- site-base/miles/source-preparation/index.html	2026-11-13 20:13:22.036137083 +0000
+++ site-pr/miles/source-preparation/index.html	2026-11-13 20:13:17.041302251 +0000
@@ -1946,6 +1946,17 @@
   
 </li>
       
+        <li class="md-nav__item">
+  <a href="#abstention-aware-factoid-qa" class="md-nav__link">
+    <span class="md-ellipsis">
+      
📄 sitemap.xml
--- site-base/sitemap.xml	2026-11-13 20:13:22.036849055 +0000
+++ site-pr/sitemap.xml	2026-11-13 20:13:17.042018149 +0000
@@ -109,6 +109,10 @@
          <lastmod>2026-11-13</lastmod>
     </url>
     <url>
+         <loc>https://github.com/allenai/open-instruct/miles/abstention/</loc>
+         <lastmod>2026-11-13</lastmod>
+    </url>
+    <url>
📄 sitemap.xml.gz
Binary files site-base/sitemap.xml.gz and site-pr/sitemap.xml.gz differ

Showing first 10 lines of diff for each changed file (up to 5 files, excluding search indices).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Claude.

Reviewed commit 4614f9ffa32b70cd7048cb0a8c271d29ab1c0f6e.

Merge recommendation: Ready to merge.

This is a well-scoped port. The adapter, profile/target binding checks, the explicit-IDK shortcut, and the docs fit the existing named-judge design. I found no blocking correctness or security problem. The first comment below changes behaviour in a way the author probably didn't intend, so I'd recommend addressing it before or soon after merge.

1. Forcing policy = "raise" makes evaluation fail on a judge outage (worth fixing)

open_instruct/miles/rewards/general_judge.py:326-328

The comment says the goal is that a failed judge is "never recorded as an incorrect answer". The existing pipeline already handles that without the override:

  • Training (rewards.py:658, omit_failures=True): under the default zero policy, general_judge_score returns 0.0 with status="judge_error". _score sees that status at rewards.py:570-574 and raises, so the response or group is left out. It is never trained on as a 0. With the override, the raise happens earlier, but the result is the same omission.
  • Evaluation (rewards.py:658, omit_failures=False, failure_policy="zero"): on base, a named judge that fails after three retries returns a judge_error zero. The eval run logs it and continues; the diagnostics show it was a judge failure, not a grade. With this PR, the abstention judge raises instead. _score re-raises at rewards.py:593-594, so one transient judge outage during periodic eval stops the whole run. Other named judges in the same mixture don't behave this way.
  • registered_reward(..., failure_policy="zero") callers are affected the same way.

Possible fix: drop the override and let _score's judge_error handling keep failures ungraded, as it does for reference_judge. If you want abstention to be stricter than the global setting, consider making it explicit only for training, for example a per-rubric failure policy. If crashing eval is intended, say so in docs/miles/abstention.md. Right now the doc only says failures "remain errors".

Related nit: the failure-diagnostics block at general_judge.py:355-367 still records "kind": "general_judge" for abstention failures. Success paths are relabelled to abstention.KIND (lines 403-406). Setting the kind on failure too would make it easier to filter abstention failures in run records.

2. The probe's third check never contacts the judge (optional)

open_instruct/miles/rewards/judge_registry.py:121-142

\boxed{[IDK]} takes the is_abstention shortcut, so scores[2] == 0.5 is true no matter what the judge does. So the check against [1.0, 0.0, 0.5] really only tests the A and B tiers. It never shows that the selected judge returns C (NOT_ATTEMPTED) for a hedged, unboxed abstention, and that is the case most likely to be miscalibrated. Consider adding a fourth control such as "I'm not sure; I don't know the capital of France." and expecting 0.5 with judge_called=True. The doc's wording, "explicit-abstention controls", is accurate today, but a reader could take it to mean the judge's C path is covered.

3. Adapter robustness (minor)

open_instruct/miles/datasets/source_preparation.py:363,365

  • raw.get("responses_create_params", {}).get("tools") raises AttributeError if the key is present but null. prepare() only catches ValueError/KeyError/TypeError/SchemaError (line 428), so one such row would abort preparation instead of being omitted. messages() already uses raw.get("responses_create_params") or {} (line 84); the same pattern would work here.
  • raw.get("domain", "HotpotQA") puts any row without a domain into the HotpotQA stratum, which would quietly skew per-domain counts. The docs call for qualification "across all four domains". Requiring domain, or falling back to something like "unknown", would keep the strata honest.

4. Small clarity nits (optional)

  • abstention.build_prompt (abstention.py:325-331) hashes the template twice. Comparing against RUBRIC_SHA256 once and returning that constant would be simpler. The drift check only catches in-process changes; an edit to the file has to update RUBRIC_SHA256 anyway. That is fine as a guard, but a one-line comment saying so would help.
  • In resolve_request, extract_answer is silently ignored for the abstention profile. That is correct, since the profile does its own think-stripping and box extraction. A short comment would stop a later reader from "fixing" it. Note that with final-answer grading on, rewards.py:530-532 already passes the response-only view, so stripping thinking again is harmless.
  • test_miles_abstention.py:1 mentions "C033", which looks like an internal tracker ID with no meaning in the repo.

What I checked

  • Read the full diff.
  • Traced general_judge_score through rewards._score, rollout_reward and registered_reward to compare the failure-policy behaviour on base and head.
  • Checked that NamedJudgeVerifier has no score_floor, so gated or truncated responses get 0 and never 0.5, as the doc says.
  • Confirmed judging.PROFILES is only consumed in judge_registry, so the new non-open-instruct/ profile doesn't affect other code paths.
  • Confirmed validate_data rejects a dict target under a generic rubric and a string target under the abstention rubric.

Verification limits

This was a static review; I ran no tests. I didn't compare the ported rubric or parser byte-for-byte against NeMo Gym at cf7f71a5. The SHA pin protects against later drift but doesn't prove the original port was faithful. Live judge calibration and GPU training are, as the PR says, still to be done.

@github-actions
github-actions Bot dismissed their stale review November 13, 2026 20:19

Drafted by Codex.

A new Codex review is starting; withdrawing the previous approval.

github-actions[bot]
github-actions Bot previously approved these changes Nov 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Codex.

Reviewed commit c7493a4d6ac1ccdbb12b47f827f42ac783c7d2bb.

No blocking findings or non-blocking correctness concerns found.

Reviewed the exact BASE...HEAD diff and head sources, including source adaptation, target/profile validation, native extraction and scoring, shared judge dispatch, grading gates, and training/evaluation failure handling. Existing generic judge behavior is preserved. There are no previous Codex findings to reassess.

Exact-head parser and rubric smoke checks passed. Ruff and pytest are unavailable in this sandbox, so I could not independently rerun the reported checks. Live judge calibration and GPU training remain unverified, as disclosed in the PR; these limits do not block approval of this scoped change.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Claude.

Reviewed commit c7493a4d6ac1ccdbb12b47f827f42ac783c7d2bb.

Merge recommendation: Ready to merge.

The follow-up commits address the substantive points from the earlier review. I found no blocking correctness or security problem. One new side effect of the diagnostics relabelling is worth fixing (item 1). The rest are minor.

Earlier review: what changed

  • Judge failure policy override: resolved. general_judge_score no longer forces raise for abstention. A judge failure now follows the global policy (general_judge.py:325-374). Training still excludes judge_error responses through rewards.py:570-574, and evaluation records a fallback zero and keeps running. test_bridge_excludes_failed_training_grades_and_continues_eval tests both paths, and the doc now describes this accurately.
  • Failure diagnostics kind: resolved (general_judge.py:361). This change causes item 1 below.
  • Probe covering the judge's C path: resolved. A fourth, unboxed hedged control is added (judge_registry.py:124). The test asserts 3 judge calls and judge_called=True on the last result.
  • Adapter robustness: resolved. It now uses responses_create_params or {}, and the domain fallback is limited to the 24-hex HotpotQA IDs, with "Unknown" for anything else (source_preparation.py:91-96).
  • resolve_request comment: resolved (judge_registry.py:45-46).
  • Still open, both minor: build_prompt hashes the template on every call (abstention.py:330), and test_miles_abstention.py:1 still says "C033".

1. Abstention judge calls and errors no longer appear in rollout/general_judge/* metrics (worth fixing)

open_instruct/miles/rewards/general_judge.py:315-317, 361, 400-403

rollout_metrics.general_judge_metrics (miles/rollout/rollout_metrics.py:88-102) only counts records where record.get("kind") == "general_judge". Abstention diagnostics now use kind = "nemo_abstention_v1" in all three cases: the explicit-IDK shortcut, failures and successes. So abstention samples never show up in rollout/general_judge/samples, errors or error_fraction.

This matters for training. If the abstention judge goes down, its judge_error responses are excluded as intended, but the judge error-rate metric won't move. The only remaining signal is the generic rollout/verification/ungraded_* counter. If abstention is the only judged source in a run, the general_judge metrics are missing entirely.

Reward exclusion is not affected: rewards.py:570 checks status, not kind. This is metrics only.

Possible fixes:

  • Keep kind="general_judge" and add a separate field such as profile="nemo_abstention_v1" or rubric_kind=... for filtering.
  • Or widen the filter in general_judge_metrics to include abstention.KIND. Consider whether explicit-IDK records (judge_called=False) should count toward "samples". I'd lean towards counting only judge calls, or adding a separate abstention/explicit counter. That way, a policy that learns to output [IDK] doesn't dilute the error fraction.

Either way, a small test in test_miles_abstention.py that runs general_judge_metrics on an abstention judge_error sample would lock this in. I raised the failure-path relabelling in the previous review without noticing this consumer, so this one is partly on me.

2. Explicit-IDK shortcut skips the failure-policy validation (nit)

general_judge.py:314-327

The early return 0.5 comes before the policy not in {"zero", "raise"} check. A misconfigured OI_MILES_JUDGE_FAILURE_POLICY therefore goes unreported until the first response that isn't [IDK]. That is harmless in practice. If you want config errors to surface on the first call regardless of the response, move the policy validation above the shortcut.

3. prompt_sha256 and rubric_sha256 are identical for this profile (question)

judge_registry.py:64, general_judge.py:392

resolve_request returns digest in both positions, so the success diagnostics record the rubric template hash as both prompt_sha256 and rubric_sha256. The per-sample rendered prompt is still distinguished by rendered_prompt_sha256. This is not new (the generic profiles already do the same), so it's only worth a comment if downstream evidence consumers treat the two fields as different things.

4. CHANGELOG placement (nit)

CHANGELOG.md:10: the entry adds a new adapter and profile but sits under "Changed". If the file uses an "Added" section in this release block, that would be the more natural place. Otherwise, ignore.

What I checked

  • Read the full diff and compared it with the prior review.
  • Traced general_judge_score through rewards._score (rewards.py:565-610) for the training-omission and evaluation zero-fallback paths.
  • Grepped every consumer of diagnostics kind and judge_error, which turned up item 1.
  • Verified that the probe's fourth control doesn't normalize to [IDK], so it reaches the judge.
  • Confirmed that prepare() catches the adapter's ValueError/KeyError paths, so a bad row is omitted rather than aborting preparation.
  • Checked that validate_data enforces the profile/target pairing in both directions.

Verification limits

This was a static review; I ran no tests. I did not compare the ported rubric and parser byte-for-byte against NeMo Gym cf7f71a5. Live judge calibration and GPU training are still to be done, as the PR says.

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Changes Detected

📄 miles/source-preparation/index.html
--- site-base/miles/source-preparation/index.html	2026-11-13 20:21:48.041871224 +0000
+++ site-pr/miles/source-preparation/index.html	2026-11-13 20:21:43.800701585 +0000
@@ -1946,6 +1946,17 @@
   
 </li>
       
+        <li class="md-nav__item">
+  <a href="#abstention-aware-factoid-qa" class="md-nav__link">
+    <span class="md-ellipsis">
+      
📄 sitemap.xml
--- site-base/sitemap.xml	2026-11-13 20:21:48.042627839 +0000
+++ site-pr/sitemap.xml	2026-11-13 20:21:43.801456126 +0000
@@ -109,6 +109,10 @@
          <lastmod>2026-11-13</lastmod>
     </url>
     <url>
+         <loc>https://github.com/allenai/open-instruct/miles/abstention/</loc>
+         <lastmod>2026-11-13</lastmod>
+    </url>
+    <url>
📄 sitemap.xml.gz
Binary files site-base/sitemap.xml.gz and site-pr/sitemap.xml.gz differ

Showing first 10 lines of diff for each changed file (up to 5 files, excluding search indices).

@github-actions
github-actions Bot dismissed their stale review November 13, 2026 20:26

Drafted by Codex.

A new Codex review is starting; withdrawing the previous approval.

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Changes Detected

📄 miles/source-preparation/index.html
--- site-base/miles/source-preparation/index.html	2026-11-13 20:27:42.739470640 +0000
+++ site-pr/miles/source-preparation/index.html	2026-11-13 20:27:37.188092933 +0000
@@ -1946,6 +1946,17 @@
   
 </li>
       
+        <li class="md-nav__item">
+  <a href="#abstention-aware-factoid-qa" class="md-nav__link">
+    <span class="md-ellipsis">
+      
📄 sitemap.xml
--- site-base/sitemap.xml	2026-11-13 20:27:42.740429228 +0000
+++ site-pr/sitemap.xml	2026-11-13 20:27:37.189056573 +0000
@@ -109,6 +109,10 @@
          <lastmod>2026-11-13</lastmod>
     </url>
     <url>
+         <loc>https://github.com/allenai/open-instruct/miles/abstention/</loc>
+         <lastmod>2026-11-13</lastmod>
+    </url>
+    <url>
📄 sitemap.xml.gz
Binary files site-base/sitemap.xml.gz and site-pr/sitemap.xml.gz differ

Showing first 10 lines of diff for each changed file (up to 5 files, excluding search indices).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Codex.

Reviewed commit ebdca5379bdad50fe3b5edf7f3306b0475e835df.

No blocking findings or other actionable concerns found.

Checked the exact BASE...HEAD diff, source adaptation, target/profile validation, native answer extraction and reward mapping, service probing, and shared grading/failure handling. Existing judge profiles retain their prior behavior; no optimizer, distributed coordination, or checkpoint paths changed. There were no previous change requests to reassess.

Validation: changed Python sources passed syntax checks; head-source abstention smoke checks and diff whitespace checks passed. Pytest and Ruff are unavailable in this sandbox. Live judge calibration and GPU training remain unverified, as disclosed in the PR.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drafted by Claude.

Reviewed commit ebdca5379bdad50fe3b5edf7f3306b0475e835df.

Merge recommendation: Ready to merge.

The latest commit (ebdca53) fixes the one substantive point from the previous review, the missing judge metrics. I found no blocking correctness or security problem. What's left is minor and optional.

Prior review: status

  • 1. Abstention judge calls missing from rollout/general_judge/* metrics: resolved. Success and failure diagnostics use kind="general_judge" again (open_instruct/miles/rewards/general_judge.py:362, 384). The profile is now carried as rubric_profile/judge_called (:363, :403-405), so general_judge_metrics (open_instruct/miles/rollout/rollout_metrics.py:88-102) counts abstention judge calls and judge errors again. The explicit-[IDK] shortcut keeps kind=abstention.KIND (general_judge.py:319). As a result, an [IDK] that never reached the judge doesn't dilute the judge error fraction. I think that is the right split. test_native_judge_rewards and test_empty_judge_is_not_an_incorrect_grade now assert kind == "general_judge".
  • 2. Explicit-IDK shortcut skipped policy validation: resolved. The policy not in {"zero", "raise"} check (general_judge.py:314-316) now runs before the shortcut (:317).
  • 3. prompt_sha256 and rubric_sha256 are the same value: unchanged. This was already true before this PR and is only a question. Nothing new here.
  • 4. CHANGELOG entry under "Changed": unchanged (CHANGELOG.md:10). This is a nit. The entry adds a new adapter and profile, so "Added" may fit better if this release block uses that heading.
  • Earlier nits, still open (optional): abstention.build_prompt re-hashes the 5 KB template on every judged sample (open_instruct/miles/rewards/abstention.py:327). Checking it once at import, or caching the result, would keep the drift guard without per-call cost. The test module docstring still says "for C033" (open_instruct/test_miles_abstention.py:1), which is an internal tracking ID with no meaning in the repo.

New observations (all minor)

  1. The explicit-IDK record has a different shape from the judged records (optional). general_judge.py:318-327 gives the shortcut record kind, status, verdict, score, judge_called, binding, rubric_sha256 and rubric_profile. It has no model, seed, temperature or rendered_prompt_sha256. I found no consumer that requires those fields (a grep for judge_called and rendered_prompt_sha256 in non-test code only turns up this file), so this is fine today. If Workbench evidence tooling later groups diagnostics by rubric_profile, it will need to treat both kind values as one profile. A one-line comment at :317 would make that contract explicit, for example: "explicit abstention is not a judge call, so it is deliberately excluded from general_judge metrics".

  2. reasoning and verdict hold the same value on the judged path (nit). For abstention, abstention.grade returns ("correct"|"incorrect"|"abstain", score), so reasoning (:395) and the verdict added at :404 are always equal. That's harmless. For other parsers, reasoning holds the judge's free text, so readers may expect it to differ here. You could leave reasoning out for this parser, or leave it as is.

  3. The test name test_unavailable_judge_is_unknown overstates its coverage (nit). test_miles_abstention.py only exercises raise mode. The zero-policy path for transport failures is covered indirectly by the empty-reply test and by test_bridge_excludes_failed_training_grades_and_continues_eval, so coverage is fine. A name like test_unavailable_judge_raises_in_strict_mode would match what the test actually does.

  4. The adapter's role check assumes plain-string content (question). source_preparation.py:358 compares ms[-1]["content"] != target["question"]. Rows with list-shaped content parts would be rejected and omitted rather than adapted. That fails closed and probably doesn't happen in this dataset, so I'm only flagging it in case the published rows ever use the content-parts form.

What I checked

  • Read the full diff and compared it with the head general_judge.py and the base behaviour, including the generic-profile paths through resolve_request. They are unchanged apart from computing parser locally (judge_registry.py:41-68).
  • Re-traced the diagnostics kind consumers in rollout_metrics.py (code_service_metrics, math_verifier_metrics, general_judge_metrics) to confirm the metrics fix.
  • Confirmed the probe's fourth control is unboxed hedging text that doesn't normalize to [IDK], so it reaches the judge. The test asserts 3 calls and judge_called=True.
  • Confirmed that validate_data enforces the profile/target pairing in both directions, and that resolve_request rejects abstention targets bound to generic rubrics.

Verification limits

This was a static review; I ran no tests. I did not compare the ported rubric and parser byte-for-byte against NeMo Gym cf7f71a5. Live judge calibration and GPU training are still to be done, as the PR description says.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant