Repository navigation
Fix UnboundLocalError in reward.py: compute_score cannot compute any reward - #7
Merged
hsaest merged 2 commits intoOct 6, 2026
Conversation
…reward
compute_score() initialises two open-ended-eval locals from themselves,
~80 lines before they are first assigned:
line 2105: kwargs.pop("openended_eval_llm_addresses", openended_eval_llm_addresses)
line 2112: os.environ.get("OPENENDED_EVAL_LLM_MODEL_NAME", openended_eval_llm_model)
line 2114: .strip() or openended_eval_llm_model
Neither name is a parameter of compute_score, and both are assigned later
in the body (2109, 2182), which makes them function-locals throughout. Since
dict.pop() evaluates its default eagerly, the read at 2105 raises
UnboundLocalError on every call, before the extracted_answer early exit and
before every scoring branch. No input, kwarg, env var or config avoids it.
compute_score is the configured reward function
(config/deepresearch_trainer.yaml: custom_reward_function.name: compute_score,
path set by run_deepresearch_fully_async_megatron.sh:1188), so no reward can
be computed by the released RL recipe.
The intended defaults are clear from the surrounding code: the fallback ladder
at 2182-2189 is written to receive None and fall back to eval_llm_addresses,
and the structurally identical inline_citation_eval_llm_model block at
2092-2097 uses eval_llm_model in both positions.
Verified: 6/6 entry-point invocations raise before the fix and return real
scores after it (objective task 1.0 base * 0.75 base_score_weight = 0.75).
pyflakes flags exactly these three reads as undefined names, and reports
none after the fix.
hsaest
approved these changes
Oct 6, 2026
hsaest
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed the three fixes and added regression tests for the default objective fallback and explicit open-ended configuration. Both tests pass locally with python -m unittest tests.recipe.deepresearch.test_reward_on_cpu -v. Approved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
compute_score()inrecipe/deepresearch/reward.py— the reward function the released RL recipe is configured to use — raisesUnboundLocalErroron every call, before any scoring happens. On a clean checkout ofmain(962e10b), no reward can be computed for any task type.Root cause
training_scripts/rl/recipe/deepresearch/reward.py,compute_score(), lines 2103–2114.Two open-ended-eval locals are initialised from themselves:
Neither
openended_eval_llm_addressesnoropenended_eval_llm_modelis a parameter ofcompute_score(its signature has noopenended_*params), and both are assigned later in the body — line 2182 and line 2109 respectively. Assignment anywhere in a function body makes a name local for the whole body, so these reads hit an unbound local.Python evaluates a call's arguments before the call, so the default expressions in
dict.pop(...)andos.environ.get(...)are evaluated eagerly whether or not the key is present. That makes the first two reads unconditional:kwargscan avoid it.OPENENDED_EVAL_LLM_MODEL_NAMEdoes not help, because the default argument is still evaluated.run_deepresearch_fully_async_megatron.sh:794exportsOPENENDED_EVAL_LLM_MODEL_NAME="${OPENENDED_EVAL_LLM_MODEL_NAME:-}", sostr("").strip()is falsy and theorbranch is taken.All three therefore need fixing, and they sit above the
if not extracted_answerearly exit at 2131 and above every scoring branch, so nothing downstream is ever reached.pyflakesflags exactly these three reads:Impact
Affected — the entire RL reward path.
compute_scoreis the configured reward function:config/deepresearch_trainer.yamlsetscustom_reward_function.name: compute_score(line 52) andpath: recipe/deepresearch/reward.py(line 50), andrun_deepresearch_fully_async_megatron.sh:1188sets the same path at launch. Because the failing read precedes all routing, every task type dies identically — objective default scoring, objective scoring via the 864 per-task scripts ineval_scripts/, open-ended rubric scoring, BrowseComp and HLE — as do both public wrappers re-exported from__init__.py. 6/6 entry-point invocations raise (see below). Consequently the inline-citation component and thebase_score * base_score_weight + citation_score_addedcombination at line 2317 never execute, and the fallback ladder at 2182–2189 is dead code today.With
reward_loop_class_name=DeepResearchRewardLoopManager(set atrun_deepresearch_fully_async_megatron.sh:1199),reward_loop_manager.py:142awaitscompute_scoreinside atrythat catches onlyasyncio.TimeoutError, so theUnboundLocalErrorpropagates out of the reward loop rather than being caught.Unaffected.
compute_score_sync()(line 1402) does not contain this pattern and is fine._compute_base_score()is fine — it takes both names as ordinary required parameters (lines 1820–1821) and is called correctly at 2214–2215. Nothing underevaluation/is affected: the offline benchmark graders are separate code and do not importreward.py. Inference (inference/) is unaffected.Present since the initial release:
git blameputs all three lines in002b75d(2026-05-20); the two later commits to this file (9ac3b3e,ac4422e) did not touch this region. Nothing in the repo exercises this function: no test importsrecipe/deepresearch/reward.py, and the othercustom_reward_function.name=compute_scoreoccurrences underrecipe/(genrm_remote,r1_ascend,spo,retool) each pointcustom_reward_function.pathat their own reward file, not at this one.run_deepresearch_fully_async_megatron.sh:1188is the only place that path isrecipe/deepresearch/reward.py.Fix
Three tokens, restoring the defaults the surrounding code already expects. This is the minimal change that makes both reads legal:
None. The fallback ladder at 2182–2189 is written to receiveNone: it assignsopenended_eval_llm_addresses = openended_eval_llm_address, optionally regenerates from IPs/ports, then doesif openended_eval_llm_addresses is None: openended_eval_llm_addresses = eval_llm_addresses.Noneis the value that makes that ladder behave as documented ("falls back to objective addresses", line 2181).eval_llm_model. The structurally identical block 17 lines above,inline_citation_eval_llm_model(2092–2097), useseval_llm_modelin both the env-default position and theorfallback. This block appears to be a copy of it with one rename too many.No behaviour change beyond making the function runnable: when a caller supplies
openended_eval_llm_address/openended_eval_llm_model(or the env var), the resolved values are exactly what the existing code intended.Verification
Reproduction script (
repro_reward_unboundlocal.py, attached below) imports the unmodifiedreward.pyfrom a clean checkout and calls each entry point with a rollout whose answer exactly matches the ground truth.reward.pyimports no verl/torch/ray, so this needs onlyaiohttp+ stdlib and no GPU, cluster or API key.Before (clean checkout of
main@962e10b):After this patch:
0.75isbase_score 1.0 * base_score_weight 0.75with the citation component at0.0, i.e. the full path now runs and returns a real score. BrowseComp and HLE return0.0witherror: browsecomp_missing_api_key/hle_missing_judge_modelbecause this environment has no judge credentials — that is the expected diagnostic, and it confirms routing now reaches those branches instead of crashing.Patching only line 2105 leaves the second bug live, which is why both are in this patch:
python -m pyflakes training_scripts/rl/recipe/deepresearch/reward.pyreports the threeundefined nameerrors above before the patch and none after it.repro_reward_unboundlocal.py(run from the repo root)