feat: super v3.5 posttraining with flexible length penalties - #3852
Conversation
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>
|
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. |
|
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. |
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>
…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
left a comment
There was a problem hiding this comment.
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.015gates 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 citesflag_reasoning_length_outliers, which doesn't exist anywhere in the repo.profiled_length_penaltyhas 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_multipliersnever checksenabled, 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_paramsis re-resolved independently in three places and the copies have drifted.sync_rollout_actor.py:262passesreward_penalty_configbut notlength_adjustment_config— the same omissionb8ca5c331fixed for the async collector. No shipped config reaches it today (nothing setsdata_plane.enabledtogether withnemo_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:ZeroDivisionErroron empty input (after the output file is written),TypeErroron an explicit"pass_rate": null, and a comment promising a "tiny parser" fallback that doesn't exist wrapped around an unreachableexcept ImportError. Alsotools/is the established home for standalone utilities._resolve_agent_paramscan never returnNone, so threeif params is Noneguards and the verbose[skipped]branch are unreachable; and itsprint()warning fires once per group per step (128 unsuppressable prints/step atnum_prompts_per_step: 128) while the module already has alogger.
Generated by Claude Code
…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>
…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>
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 — appliedin both sync and async GRPO paths.
Issues
None. This is the
super-v3.5-posttrainingport of the length-adjustment work targetingmainin #3604, plus fixes made since (async rollout propagation, profile-band clamp atf, global profile-band defaults, unit tests).Usage