Skip to content

feat: super v3.5 posttraining with flexible length penalties - #3852

Merged
yfw merged 27 commits into
super-v3.5-posttrainingfrom
adithyare/super_v3.5_posttraining_lenpen
Aug 27, 2026
Merged

feat: super v3.5 posttraining with flexible length penalties#3852
yfw merged 27 commits into
super-v3.5-posttrainingfrom
adithyare/super_v3.5_posttraining_lenpen

Conversation

@arendu

@arendu arendu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

Adds configurable length penalties/bonuses for rollouts (reward-mutation and GDPO reward-feature modes) to reduce verbosity in RLVR training — including group-relative length scaling, zMAD outlier penalties, profile-band multipliers with per-prompt or global {a, b, f} bands, and per-prompt profile-gated group-relative scaling — applied
in both sync and async GRPO paths.

Issues

None. This is the super-v3.5-posttraining port of the length-adjustment work targeting main in #3604, plus fixes made since (async rollout propagation, profile-band clamp at f, global profile-band defaults, unit tests).

Usage

# Reward-mutation mode: dense group-relative length pressure, gated per prompt
grpo:
  length_bonus:
    default:
      enabled: true
      length_type: tokens
      group_total_length_penalty_coeff: 0.1
      group_length_penalty_profile_gate: true          # penalty only when group mean length
      group_length_penalty_profile_gate_channel: total #   exceeds the prompt's profiled target
      group_length_penalty_profile_gate_field: a
    profile_band:            # global {a,b,f} fallback for datasets without per-prompt bands
      enabled: true
      defaults:
        total: {a: 18000, b: 24000, f: 0.9}
    agent_overrides:
      instruction_following_simple_agent:
        enabled: false

# GDPO feature mode: same signals as a separate reward feature, env reward untouched
grpo:
  adv_estimator:
    name: gdpo
    reward_features:
      default:
        env_reward: 1.0
        length_adjusted_reward:
          group_total_length_penalty_coeff: 0.1
        think_count_delta: 1.0

Full algorithm reference: nemo_rl/utils/length_adjustments.md.

Before your PR is "Ready for review"

Pre checks:
- [x] Make sure you read and followed Contributor guidelines
- [x] Did you write any new necessary tests?
- [x] Did you run the unit tests and functional tests locally? Visit our Testing Guide for how to run tests
- [x] Did you add or update any necessary documentation? Visit our Document Development Guide for how to write, build and test the docs.
# Additional Information
* ...

arendu and others added 8 commits August 26, 2026 10:52
Token ids are tokenizer-specific, so defaulting the think_close token id
to 13 could count unrelated tokens (e.g. newlines) on other tokenizers
and silently inject large spurious think_count_delta penalties. Token
counting now only runs when the id is configured; when no counting
method is available the feature stays neutral instead of guessing.

Also aligns kwargs at the rollout call site and documents the Phase-3
profiled-penalty gate in length_adjustments.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AsyncTrajectoryCollector called run_async_nemo_gym_rollout without
length_adjustment_config, so async_grpo_train silently skipped all
configured length bonuses/penalties (both grpo.length_bonus mode and
GDPO reward_features mode). Pass master_config.grpo like the sync
grpo_train and validate paths already do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to 0

Previously the linear slope from (a, 1.0) through (b, f) continued past b
until the multiplier hit 0, fully wiping rewards for long-but-correct
rollouts. Now the multiplier floors at f for all lengths >= b, so the
maximum penalty is bounded by the configured f.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New config block length_bonus.profile_band with enabled + defaults holding
per-channel {a, b, f}. Channels listed under defaults are implicitly
enabled; per-row profile_band metadata still wins per-channel when present.
The global band also feeds the profile-gated group-relative penalty.
Malformed channel blocks (missing keys or b <= a) are ignored with a
warning. Previously profile_band silently no-oped when the dataset lacked
per-prompt metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
22 tests covering the two production length-penalty algorithms:
- _band_multiplier shape: 1.0 up to a, linear to f at b, clamped at f
  past b, no-op on missing/malformed channels
- profile_band via per-row metadata: total/reasoning channels, positive-
  reward gating, missing-band and disabled-channel no-ops
- profile_band via global config defaults: implicit channel enabling,
  works without a length_bonus.default block, per-row precedence,
  disabled block and malformed (b <= a) channel no-ops
- group relative-length scaling: symmetric zero-centered adjustments,
  mean preservation, equal-length and zero-coefficient no-ops,
  positive-only participation, reasoning-channel isolation,
  agent_overrides disable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l-band threshold source

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@arendu
arendu marked this pull request as ready for review August 26, 2026 18:46
@arendu
arendu requested review from a team as code owners August 26, 2026 18:46
@copy-pr-bot

copy-pr-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

Auto-sync is disabled for ready for review pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@arendu
arendu requested a review from yfw August 26, 2026 18:47
Remove the ultra recipe configs, the pipeclean launcher, and the example
profile_band.yaml. The PR carries only the length-adjustment feature code,
tests, and docs; recipe configs ship separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arendu arendu changed the title Adithyare/super v3.5 posttraining lenpen feat: super v3.5 posttraining with flexible length penalties Aug 26, 2026
…llouts

Previously, when fewer than profiled_length_min_samples profiled rollouts
passed, the threshold fell back to ALL profiled lengths — deriving a length
budget from a profile that failed the problem, then applying a flat penalty
to every current rollout over it (correct ones included, since this
algorithm has no reward gate on the receiving side). A problem the profiled
model could not solve likely needs a larger thinking budget, not a penalty
computed from failed attempts.

Now: fewer than min_samples passing profiled rollouts -> no profiled
penalty for that prompt. min_samples semantics change from 'samples needed
to compute the threshold (any)' to 'passing samples required to apply the
penalty'. Set profiled_length_min_samples: 1 to opt in to single-pass
thresholds. Adds 4 unit tests; updates the doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@yfw yfw 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.

Local review by a multi-agent pass (RL/config, reward-math, bug-scan, tests, design, plus an adversarial verification round). Every numeric claim below was executed, not inferred.

The feature's length_bonus half looks sound — I verified that group-relative scaling preserves the group mean exactly, that degenerate groups (size 1, all-equal lengths, all-zero or all-negative rewards, zero lengths) are all safe with no NaN created and no division by zero, that _band_multiplier's boundaries are correct including the past-b clamp from 25df5bfcc, that the zMAD statistic uses the correct Iglewicz–Hoaglin constant, and that a config which doesn't opt in is an exact no-op on rewards. The 661-line design doc is genuinely above the bar for this repo.

The GDPO feature half is where the problems are, and two of them need fixing before merge: the dispatch hijack (which affects existing configs with no opt-in) and the fact that the documented config shape produces no length pressure at all. Details inline.

Follow-ups — not asking for these in this PR:

  • _ZMAD_MIN_MAD_REL = 0.015 gates the whole channel rather than suppressing noise: lengths [20000, 20100, 19900, 20050, 19950, 32768] give MAD/median = 0.0037 < 0.015, so a modified-Z of 114.6 is missed — precisely the tight-distribution-with-one-runaway case the channel exists for. The justifying comment cites flag_reasoning_length_outliers, which doesn't exist anywhere in the repo.
  • profiled_length_penalty has no positive-reward gate, unlike every other channel, so an all-wrong group gains within-group variance (and a gradient) where GRPO previously produced none.
  • _apply_profile_band_multipliers never checks enabled, unlike Phase 1 at L353. Currently masked — skipped groups leave lengths at 0 and _band_multiplier(0, ch) returns 1.0 — but it's a live guard gap. Same root as Phase 2 printing coefficients the profile gate had already zeroed: _resolve_agent_params is re-resolved independently in three places and the copies have drifted.
  • sync_rollout_actor.py:262 passes reward_penalty_config but not length_adjustment_config — the same omission b8ca5c331 fixed for the async collector. No shipped config reaches it today (nothing sets data_plane.enabled together with nemo_gym), so no impact, but a parametrised "every rollout producer threads the config" test would stop this recurring a third time.
  • scripts/build_profile_band_dataset.py: ZeroDivisionError on empty input (after the output file is written), TypeError on an explicit "pass_rate": null, and a comment promising a "tiny parser" fallback that doesn't exist wrapped around an unreachable except ImportError. Also tools/ is the established home for standalone utilities.
  • _resolve_agent_params can never return None, so three if params is None guards and the verbose [skipped] branch are unreachable; and its print() warning fires once per group per step (128 unsuppressable prints/step at num_prompts_per_step: 128) while the module already has a logger.

Generated by Claude Code

Comment thread nemo_rl/algorithms/advantage_estimator.py Outdated
Comment thread nemo_rl/utils/length_adjustments.py Outdated
Comment thread nemo_rl/algorithms/grpo.py Outdated
Comment thread nemo_rl/utils/length_penalty.py
Comment thread nemo_rl/utils/length_adjustments.py Outdated
Comment thread examples/nemo_gym/grpo_nanov3.yaml Outdated
arendu and others added 10 commits August 26, 2026 19:53
…yaml

Accidental passenger from the original port; the flag is deliberately
managed per-recipe (review feedback on #3852).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion only

Review feedback on #3852: the feature-mode layer dispatched on batch key
presence (hijacking existing GDPO reward/* runs) and its documented config
shape parsed to zero params. Remove the layer entirely rather than patch it
under review; it can return as its own properly-gated PR.

- advantage_estimator.py, test_grpo.py, test_reward_penalties.py: reverted
  to the base branch versions (existing GDPO estimator untouched)
- rollouts.py: drop gdpo_reward_features recording/metrics/batch key and
  the think_count_delta feature; keep length_adjustment_config threading
- length_adjustments.py: drop reward_features merge and feature recording;
  grpo.length_bonus reward-mutation mode is the single code path
- length_adjustments.md: remove GDPO feature mode sections

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The length-adjustment code groups by the training stride
(num_generations_per_prompt), which mismatches
val_num_generations_per_prompt — groups straddled distinct prompts and
shaped rewards corrupted accuracy/pass_k (review finding on #3852).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…entile default

Review findings on #3852:
- profile_band multipliers now skip rollouts whose additive-adjusted base
  is <= 0: scaling a negative base by m < 1 RAISED rewards for longer
  rollouts (incentive inversion)
- unknown keys anywhere under grpo.length_bonus now raise ValueError
  instead of being silently ignored
- top_percentile gets one canonical default (0.5, matching the docs); the
  previous generic 0.0 default made the later setdefault(0.2) dead code
- global profile_band defaults no longer override an explicitly configured
  profile_band_<channel>: false

Adds 4 regression tests (30 total in the file).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopt the clamping alternative from review (#3852): stacked flat penalties
exceeding the reward now produce 0, never a negative value — length
penalties may wipe a correct rollout's reward out but not flip its sign.
The clamp applies only to originally-positive rollouts, so an env's own
negative rewards pass through untouched. The base<=0 gate in the band
phase stays as a second line of defense against the multiplier inversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The copy loop ran for every NeMo-Gym rollout; now the postprocess block is
a strict no-op unless grpo.length_bonus is configured. Also drop the
profiled_reasoning_lengths / profiled_answer_lengths copies — nothing in
the runtime consumes them (only the offline dataset builder reads those
names, directly from the JSONL).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every algorithm and the final clamp assume binary rewards; enforcing it
removes the graded/negative-reward edge cases wholesale. A prompt group
containing any non-binary reward is skipped untouched (no adjustment, no
clamp) with a warning logged once per agent — a graded judge agent in a
mixed batch degrades loudly instead of killing the run.

With binariness guaranteed the phase-3 clamp is now unconditional, which
also closes the profiled_length_penalty asymmetry from review follow-up:
penalty-created negatives on wrong rollouts floor back to 0, so an
all-wrong group stays variance-free and contributes no GRPO gradient,
matching vanilla behavior.

Adds binary-skip, graded-skip, and all-wrong-variance-free tests (34
total); documents the requirement and the top_percentile inertness under
binary rewards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…docs)

Rename before anyone depends on the key — the feature is new to this PR:
- config: grpo.length_bonus -> grpo.length_penalty
- module: nemo_rl/utils/length_adjustments.{py,md} -> length_penalty.{py,md}
- entry point: apply_group_length_adjustments -> apply_group_length_penalties
- threaded kwarg: length_adjustment_config -> length_penalty_config
- tests: test_length_adjustments.py -> test_length_penalty.py

Pure mechanical rename; no behavior change (34/34 tests unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements -w_len * R_len(y_i) with R_len = rho_q * |y_i| / l_max, where
rho_q is the prompt group's pass rate and l_max is the group's longest
total rollout length (not a hyperparameter — self-normalizing per group;
the longest correct rollout loses exactly w * rho_q).

Dense pressure scaled by difficulty: easy prompts get strong shortening
pressure, hard prompts little, and all-wrong groups (rho_q = 0) exactly
none — preserving the no-gradient-without-correctness-signal invariant by
construction. Applied to correct rollouts only, consistent with the
module; single knob pass_rate_length_penalty_weight.

Adds 5 unit tests (39 total) and the doc section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
l_max over the whole group let long wrong rambles dilute the penalty
spread between correct rollouts — the only component GRPO's mean-centered
advantage actually sees — and made 'longest correct loses exactly w*rho'
hold only when the longest rollout happened to be correct. Normalizing
over correct rollouts only decouples the pressure from wrong-rollout
noise and makes the invariant exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI runs `pre-commit run --all-files` (.github/workflows/cicd-main.yml:387),
which failed on four hooks for this branch:

- no-underscore-md: nemo_rl/utils/length_penalty.md was the sole offender.
  This hook is a hard exit 1 and is not auto-fixable. Renamed to
  length-penalty.md, matching the hyphenated sibling docs under nemo_rl/.
  No references to the old filename exist anywhere in the tree.
- ruff (F541): dropped a needless f-string prefix in
  scripts/build_profile_band_dataset.py.
- ruff check --select I: reordered the length_penalty import in
  nemo_rl/experience/rollouts.py.
- ruff-format: reflowed 3 files.

Verified: `pre-commit run --all-files` now exits 0; the 4 touched files
AST-compare identical to their previous versions apart from the intended
F541 fix and the import reorder; tests/unit/utils/test_length_penalty.py
is 39 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 27, 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/utils/length_penalty.py Outdated
arendu and others added 6 commits August 27, 2026 12:49
…cy, overridable sandbox command

--gres honors GPUS_PER_NODE (HSG GB200 has 4 GPUs/node; ray.sub already
consumes the same variable), SLURM_COMMENT enables idle-reaper exemptions
required by async-GRPO buffer fill, SLURM_QOS and SLURM_EXTRA_DEPENDENCY
are optional passthroughs, and SANDBOX_COMMAND can be overridden for
sandbox startup retry wrappers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When RESULTS_DIR is set: checkpoints/ (stable dir for singleton
auto-resume), runs/<timestamp>/{logs,slurm} with a runs/latest symlink,
ray_logs/ as ray.sub's BASE_LOG_DIR, sbatch --output/--error into the
run's slurm/, and nemo_gym_log_dir under the run's logs. Unset keeps the
legacy snapshot-relative results/ and logs/ dirs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h's schema

Ported from the geshen/nano launcher but this branch's NemoGym config
does not define it; OmegaConf struct mode rejects the override and both
ablation primers (6580116/6580230) died at config load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ne-derived configs

env.nemo_gym.uv_venv_dir / skip_venv_if_present exist in stage1-based
configs but not pipeline-derived ones; plain overrides fail Hydra
composition on the latter (killed ablation r1 and r2: 6580116/6580230,
6582890/6582899 — r1 was misattributed to nemo_gym_log_dir, which was
merely later in the same list). '++' sets the key in both cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early return read enabled with default False while per-group param
resolution used default True — a default: block omitting enabled silently
no-oped, and adding an unrelated empty agent_overrides entry flipped the
same block active (review finding on #3852). A configured default: block
is now intent-to-enable at both read sites; set enabled: false to opt out.
Two regression tests cover the repro and the unrelated-key independence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yfw
yfw merged commit 0ca2230 into super-v3.5-posttraining Aug 27, 2026
9 of 10 checks passed
@yfw
yfw deleted the adithyare/super_v3.5_posttraining_lenpen branch August 27, 2026 20:20
@yfw
yfw restored the adithyare/super_v3.5_posttraining_lenpen branch August 27, 2026 20:20
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.

2 participants