Skip to content

feat: configurable GDPO per-reward weights and multi-reward NeMo Gym bridge - #2680

Merged
terrykong merged 6 commits into
NVIDIA-NeMo:mainfrom
anjalibshah:gdpo-weights-and-gym-multireward-upstream
Jul 21, 2026
Merged

feat: configurable GDPO per-reward weights and multi-reward NeMo Gym bridge#2680
terrykong merged 6 commits into
NVIDIA-NeMo:mainfrom
anjalibshah:gdpo-weights-and-gym-multireward-upstream

Conversation

@anjalibshah

Copy link
Copy Markdown
Contributor

Summary

Two GDPO (arXiv:2601.05242) capability additions on top of the existing GDPO support (#2069).

1. Configurable per-reward weights. GDPO's aggregation A = Σ wₙ·Aₙ is now tunable via grpo.adv_estimator.reward_weights (one entry per component, ordered to match reward1, reward2, …). Defaults to equal weights (all 1.0) for backward compatibility; a wrong-length list raises ValueError. Files: nemo_rl/algorithms/advantage_estimator.py, nemo_rl/algorithms/grpo.py, docs/guides/grpo.md.

2. Multi-reward NeMo Gym bridge. run_async_nemo_gym_rollout now surfaces per-component rewards (reward1…rewardN) when a Gym verifier returns reward_components, mirroring the native multi-reward rollout path. Single-reward environments are unaffected (fall back to the scalar reward). Files: nemo_rl/environments/nemo_gym.py (new extract_reward_components), nemo_rl/experience/rollouts.py, examples/nemo_gym/gdpo_multireward.yaml.

Dependency

The bridge consumes the reward_components field added in NVIDIA-NeMo/Gym#1525 — that should land first. Until it does, the bridge code is inert (single-reward fall-back), so this PR is safe to merge independently.

Test plan

Validated in the NeMo-RL container (8×H100):

  • tests/unit/algorithms/test_grpo.py::test_gdpo_advantage_estimator_reward_weightsPASSED
  • tests/unit/environments/test_nemo_gym.py::test_extract_reward_componentsPASSED

@anjalibshah
anjalibshah requested review from a team as code owners June 3, 2026 23:15
@copy-pr-bot

copy-pr-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread nemo_rl/experience/rollouts.py Outdated
Comment thread nemo_rl/experience/rollouts.py Outdated

@ZhiyuLi-Nvidia ZhiyuLi-Nvidia 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.

Generally looks good to me. Only 2 comments here.

@ZhiyuLi-Nvidia ZhiyuLi-Nvidia added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Jul 15, 2026
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

/ok to test cf64525

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

Also could you rebase on to main branch after resolving conflicts?

anjalibshah and others added 2 commits July 16, 2026 10:09
…bridge

Add reward_weights to the GDPO advantage estimator so components aggregate as
A = sum_n w_n * A_n (defaults to equal weights; wrong length raises). Extend the
NeMo Gym bridge to surface per-component rewards (reward1..rewardN) from a verifier
that returns reward_components, mirroring the native multi-reward rollout path, so
GDPO can train on NeMo Gym environments. Includes unit tests and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anjali Shah <anjshah@nvidia.com>
…ard env

Name the ready-made multi-reward reference env (NVIDIA-NeMo/Gym) so users know
exactly what to set env.nemo_gym to for an end-to-end GDPO multi-reward run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Anjali Shah <anjshah@nvidia.com>
@anjalibshah
anjalibshah force-pushed the gdpo-weights-and-gym-multireward-upstream branch from 40fc0fa to fe17cee Compare July 16, 2026 17:11
@anjalibshah

Copy link
Copy Markdown
Contributor Author

Also could you rebase on to main branch after resolving conflicts?

yes done!

@anjalibshah

Copy link
Copy Markdown
Contributor Author

Generally looks good to me. Only 2 comments here.

Thanks @ZhiyuLi-Nvidia. Resolved conflicts and rebased. Please merge, thanks.

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

Found DCO failure.

Could you finally git commit / amend -s to sign the commits?

ZhiyuLi-Nvidia
ZhiyuLi-Nvidia previously approved these changes Jul 16, 2026
…ate reward == sum(components)

Addresses review on NVIDIA-NeMo#2680:
- Stop silently overwriting final_batch["total_reward"] with the component
  sum. Keep the verifier's scalar `reward`. When reward_components is present
  the contract is reward == sum(components), so overwriting is a no-op in the
  correct case and only masks a misconfigured verifier otherwise. Validate the
  contract instead and raise on a real mismatch.
- Expand the sorted() comment to explain it is a canonical ordering (stable
  reward{n} -> component mapping for the GDPO per-component baseline and
  reward_weights), not just deduplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Anjali Shah <anjshah@nvidia.com>
@anjalibshah
anjalibshah force-pushed the gdpo-weights-and-gym-multireward-upstream branch from fe17cee to cb7f6b5 Compare July 16, 2026 17:26
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

/ok to test cb7f6b5

- Move the reward/component-sum consistency check into
  validate_reward_components_match_scalar() in nemo_gym.py, next to
  extract_reward_components, so it is unit-testable without the full rollout
  machinery. run_async_nemo_gym_rollout now calls the helper.
- Add test_validate_reward_components_match_scalar covering the matching,
  float-tolerance, single-reward-skip, and mismatch-raises cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Anjali Shah <anjshah@nvidia.com>
@anjalibshah
anjalibshah force-pushed the gdpo-weights-and-gym-multireward-upstream branch from cb7f6b5 to de41394 Compare July 17, 2026 17:52
@anjalibshah

Copy link
Copy Markdown
Contributor Author

/ok to test de41394

@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

/ok to test de41394

@yuki-97 yuki-97 removed the CI:docs Run doctest label Jul 18, 2026

@yuki-97 yuki-97 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.

thanks @anjalibshah for supporting the two features! left some comments.

Comment thread nemo_rl/experience/rollouts.py Outdated
Comment thread examples/nemo_gym/gdpo_multireward.yaml Outdated
Comment thread docs/guides/grpo.md
…eward2)

main's get_gdpo_reward_component_keys() selects batch keys by
`k.startswith("reward/")`, but the NeMo Gym bridge emitted positional
`reward1, reward2, ...`. Those never match, so GDPO saw zero components and
raised "GDPO requires multiple reward components". Emit `reward/<name>`
instead, matching the native multi-reward path.

- rollouts.py: emit final_batch[f"reward/{name}"] (component name carries
  identity; get_gdpo_reward_component_keys sorts, so no positional index).
- test_grpo.py: reward_weights test now uses reward/correctness, reward/format.
- Reword reward_weights ordering from "reward1, reward2, ..." to "alphabetical
  by component name" across advantage_estimator.py, grpo.py, grpo.md, and
  gdpo_multireward.yaml; update nemo_gym.py docstring.
- Remove dead private-fork link (gym-multireward-scope.md) from the recipe yaml.

Addresses review feedback on NVIDIA-NeMo#2680.

Signed-off-by: Anjali Shah <anjshah@nvidia.com>
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

/ok to test fce0393

copy-pr-bot Bot pushed a commit that referenced this pull request Jul 20, 2026
Guards the reward_weights aggregation path added to GDPOAdvantageEstimator (per
review on #2680). Adds a DTensor GDPO recipe over the native math_multi_reward
env (correctness / format / integer components), Qwen2.5-1.5B on GSM8K, 1n8g,
with reward_weights=[1.0, 0.5, 0.25]. Follows the nightly pattern from #1866:

- examples/configs/recipes/llm/gdpo-qwen2.5-1.5b-1n8g-fsdp2-reward-weights.yaml
- tests/test_suites/llm/gdpo-qwen2.5-1.5b-1n8g-fsdp2-reward-weights.sh
- register in tests/test_suites/nightly.txt
- add "gdpo" -> gdpo_math_1B.yaml to ALGO_MAPPING_TO_BASE_YAML so the recipe
  name-prefix check passes.

Signed-off-by: Anjali Shah <anjshah@nvidia.com>
Guards the reward_weights aggregation path added to GDPOAdvantageEstimator (per
review on NVIDIA-NeMo#2680). Adds a DTensor GDPO recipe over the native math_multi_reward
env (correctness / format / integer components), Qwen2.5-1.5B on GSM8K, 1n8g,
with reward_weights=[1.0, 0.5, 0.25]. Follows the nightly pattern from NVIDIA-NeMo#1866:

- examples/configs/recipes/llm/gdpo-qwen2.5-1.5b-1n8g-fsdp2-reward-weights.yaml
- tests/test_suites/llm/gdpo-qwen2.5-1.5b-1n8g-fsdp2-reward-weights.sh
- register in tests/test_suites/nightly.txt
- add "gdpo" -> gdpo_math_1B.yaml to ALGO_MAPPING_TO_BASE_YAML so the recipe
  name-prefix check passes.

Signed-off-by: Anjali Shah <anjshah@nvidia.com>
@anjalibshah
anjalibshah force-pushed the gdpo-weights-and-gym-multireward-upstream branch from fce0393 to 9143bf2 Compare July 20, 2026 22:06
@ZhiyuLi-Nvidia

Copy link
Copy Markdown
Contributor

/ok to test 9143bf2

@terrykong terrykong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the quick turnaround on the review feedback — both follow-up commits were verified locally at head fce0393c:

  • d00b431 fully resolves the reward/<name> key-scheme issue: the previously-failing test_gdpo_advantage_estimator_reward_weights now passes, a repo-wide search finds no remaining reward1/reward2 references, all ordering docs/comments/error messages are reworded consistently, and the dead skill link is gone. Nice, thorough fix.
  • fce0393 adds a well-plumbed native-path nightly (recipe resolution, naming registration, and alphabetical weight→component mapping all verified). Two follow-ups inline: the recipe yaml currently fails the minimize-check pre-commit hook (CI blocker, one-command fix), and the Gym bridge block itself still has no test coverage since the nightly runs the native math_multi_reward path.

This review was produced by a 5-agent team (RL expert, test agent, bug finder, comment reviewer, devil's advocate); all findings were independently verified, and all other pre-commit hooks (ruff, ruff-format, pyrefly) pass at head.

Relationship to #2681 (same author): no code dependency, but the demo script there feeds reward1/reward2 keys into GDPOAdvantageEstimator, so with the key scheme now settled as reward/<name> it will need the matching update.

Generated by Claude Code

Comment thread tests/test_suites/nightly.txt
Comment thread nemo_rl/experience/rollouts.py
@terrykong
terrykong enabled auto-merge (squash) July 21, 2026 07:00
@terrykong
terrykong merged commit 9f701f0 into NVIDIA-NeMo:main Jul 21, 2026
78 of 79 checks passed
@terrykong

Copy link
Copy Markdown
Collaborator

copy-pr-bot Bot pushed a commit that referenced this pull request Jul 21, 2026
…-experiment skill

Per review on #2681: experiment-specific content doesn't fit the repo's skill
convention (existing skills are general infra/process workflows). Remove the
skills/nemo-rl-gdpo-experiment/ directory and fold the genuinely additive
pieces into the existing GDPO section of docs/guides/grpo.md:
- matched GDPO-vs-GRPO comparison workflow (points at the examples/ control
  config and CPU collapse demo, both unchanged)
- what to measure (per-reward convergence, per-prompt advantage spread)
- practical notes: reward_scaling applies per component; final batch
  normalization always runs regardless of normalize_rewards

Deliberately excludes reward_weights guidance to avoid overlap with #2680,
which documents that option. Env-authoring contract is left in
docs/guides/environments.md where it already lives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Anjali Shah <anjshah@nvidia.com>
yfw added a commit that referenced this pull request Aug 24, 2026
`spinup_server` is not a field Gym has ever read. It is absent from
nemo_gym/server_utils.py at both the pin it was written against
(d67ad6611, via #2680) and the current pin (c3bac9631), and nothing on the
config MRO overrides pydantic's default extra="ignore" -- the only model_config
in the chain is ConfigDict(arbitrary_types_allowed=True) at server_utils.py:295
-- so the key was silently discarded on every load.

What actually decides whether a server is launched is the presence of an
entrypoint (cli/env.py:407, unchanged since d67ad6611:188). These thin clients
carry one, so they do spin up; the dry run simply returns early at
server_utils.py:698 before serving. The conclusion in the previous comment was
right -- validation is unconditional -- but the reason given described a
mechanism that does not exist, which made a dead key read as load-bearing.

Drop the six occurrences and reword the comment to the real mechanism. Upstream
Gym's own resources_servers/jailbreak_detection/configs/safety_judge_model.yaml
defines the same server without the key, and it has no reader anywhere in this
repo.

No behaviour change: tests/unit/environments/test_nemo_gym_prefetch_configs.py
still reports 7 passed, 2 skipped against the edited configs, which is the
expected result if the key was genuinely inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
yfw added a commit that referenced this pull request Aug 25, 2026
`spinup_server` is not a field Gym has ever read. It is absent from
nemo_gym/server_utils.py at both the pin it was written against
(d67ad6611, via #2680) and the current pin (c3bac9631), and nothing on the
config MRO overrides pydantic's default extra="ignore" -- the only model_config
in the chain is ConfigDict(arbitrary_types_allowed=True) at server_utils.py:295
-- so the key was silently discarded on every load.

What actually decides whether a server is launched is the presence of an
entrypoint (cli/env.py:407, unchanged since d67ad6611:188). These thin clients
carry one, so they do spin up; the dry run simply returns early at
server_utils.py:698 before serving. The conclusion in the previous comment was
right -- validation is unconditional -- but the reason given described a
mechanism that does not exist, which made a dead key read as load-bearing.

Drop the six occurrences and reword the comment to the real mechanism. Upstream
Gym's own resources_servers/jailbreak_detection/configs/safety_judge_model.yaml
defines the same server without the key, and it has no reader anywhere in this
repo.

No behaviour change: tests/unit/environments/test_nemo_gym_prefetch_configs.py
still reports 7 passed, 2 skipped against the edited configs, which is the
expected result if the key was genuinely inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
yfw added a commit that referenced this pull request Aug 25, 2026
`spinup_server` is not a field Gym has ever read. It is absent from
nemo_gym/server_utils.py at both the pin it was written against
(d67ad6611, via #2680) and the current pin (c3bac9631), and nothing on the
config MRO overrides pydantic's default extra="ignore" -- the only model_config
in the chain is ConfigDict(arbitrary_types_allowed=True) at server_utils.py:295
-- so the key was silently discarded on every load.

What actually decides whether a server is launched is the presence of an
entrypoint (cli/env.py:407, unchanged since d67ad6611:188). These thin clients
carry one, so they do spin up; the dry run simply returns early at
server_utils.py:698 before serving. The conclusion in the previous comment was
right -- validation is unconditional -- but the reason given described a
mechanism that does not exist, which made a dead key read as load-bearing.

Drop the six occurrences and reword the comment to the real mechanism. Upstream
Gym's own resources_servers/jailbreak_detection/configs/safety_judge_model.yaml
defines the same server without the key, and it has no reader anywhere in this
repo.

No behaviour change: tests/unit/environments/test_nemo_gym_prefetch_configs.py
still reports 7 passed, 2 skipped against the edited configs, which is the
expected result if the key was genuinely inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Yi-Fu Wu <yifu.wu@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants