Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7f013f8
Port GDPO length adjustment features to GitHub main
arendu Aug 12, 2026
2407f73
fix: require explicit think_close token id for think-count feature
arendu Aug 12, 2026
a55eb22
chore: add copyright header to build_profile_band_dataset.py
arendu Aug 12, 2026
b8ca5c3
fix: propagate length_adjustment_config to async NeMo-Gym rollouts
arendu Aug 24, 2026
25df5bf
feat: profile_band multiplier clamps at f past b instead of decaying …
arendu Aug 24, 2026
4d73c5b
feat: global profile_band defaults for datasets without per-prompt bands
arendu Aug 24, 2026
c0f605f
test: unit tests for profile_band and group relative-length scaling
arendu Aug 26, 2026
f8724c5
docs: document profile-gate defaults, fail-closed behavior, and globa…
arendu Aug 26, 2026
50244bc
chore: drop example/recipe configs from the lenpen PR
arendu Aug 26, 2026
16e4d84
fix: profiled_length_penalty requires min_samples PASSING profiled ro…
arendu Aug 26, 2026
f02100f
revert: restore should_log_nemo_gym_responses default in grpo_nanov3.…
arendu Aug 27, 2026
541be20
refactor: strip GDPO feature mode; length adjustments are reward-muta…
arendu Aug 27, 2026
87bea4f
fix: keep validation metrics on raw env reward
arendu Aug 27, 2026
bbcc4d9
fix: band multiplier negative-base guard, config validation, top_perc…
arendu Aug 27, 2026
7a4d888
feat: clamp length-adjusted rewards at 0 for correct rollouts
arendu Aug 27, 2026
b4d4b73
refactor: gate per-row metadata copying on an actual length_bonus config
arendu Aug 27, 2026
ded553a
feat: restrict length adjustments to binary (0/1) env rewards
arendu Aug 27, 2026
aa2a1a8
refactor: rename length_bonus -> length_penalty (config key, module, …
arendu Aug 27, 2026
44de07d
feat: pass-rate-scaled length penalty (MAI-style)
arendu Aug 27, 2026
89251d4
fix: pass-rate penalty normalizes by longest CORRECT rollout
arendu Aug 27, 2026
c89b83f
style: satisfy pre-commit hooks (rename doc, ruff format/isort, F541)
yfw Aug 27, 2026
3973211
feat(launcher): 4-GPU-node support, sbatch comment/qos/extra-dependen…
arendu Aug 27, 2026
d202b07
feat(launcher): optional stable RESULTS_DIR layout
arendu Aug 27, 2026
49746de
feat(launcher): optional --segment sbatch flag with divisibility guard
arendu Aug 27, 2026
9901ae3
fix(launcher): drop nemo_gym_log_dir override — key not in this branc…
arendu Aug 27, 2026
79470b5
fix(launcher): force-override (++) gym venv keys — absent from pipeli…
arendu Aug 27, 2026
1643600
fix: enabled defaults True consistently in apply_group_length_penalties
arendu Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions examples/nemo_gym/nemotron-3-super/super_launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,26 @@ fi
# ---- Derived paths ----
CODE_DIR=$(realpath "$PWD")
WANDB_NAME="${EXP_NAME}"
CHECKPOINT_DIR="results/${EXP_NAME}"
LOG_DIR="logs/${EXP_NAME}"
# Optional stable results layout: set RESULTS_DIR to get
# $RESULTS_DIR/checkpoints (stable -> singleton auto-resume)
# $RESULTS_DIR/runs/<ts>/{logs,slurm} (per-submission; runs/latest symlink)
# $RESULTS_DIR/ray_logs/<jobid>-logs (ray.sub infra logs via BASE_LOG_DIR)
# Unset: legacy snapshot-relative results/ and logs/ dirs.
RESULTS_DIR="${RESULTS_DIR:-}"
if [[ -n "${RESULTS_DIR}" ]]; then
CHECKPOINT_DIR="${CHECKPOINT_DIR:-${RESULTS_DIR}/checkpoints}"
RUN_DIR="${RESULTS_DIR}/runs/$(date +%Y%m%d-%H%M)"
LOG_DIR="${RUN_DIR}/logs"
SLURM_LOG_DIR="${RUN_DIR}/slurm"
mkdir -p "${CHECKPOINT_DIR}" "${LOG_DIR}" "${SLURM_LOG_DIR}"
ln -sfn "$(realpath "${RUN_DIR}")" "${RESULTS_DIR}/runs/latest"
export BASE_LOG_DIR="${BASE_LOG_DIR:-${RESULTS_DIR}/ray_logs}"
mkdir -p "${BASE_LOG_DIR}"
else
CHECKPOINT_DIR="results/${EXP_NAME}"
LOG_DIR="logs/${EXP_NAME}"
SLURM_LOG_DIR=""
fi

VLLM_CACHE_DIR="${PERSISTENT_CACHE}/vllm_compile_cache"
FLASHINFER_CUBIN_CACHE="${PERSISTENT_CACHE}/flashinfer_cubins"
Expand Down Expand Up @@ -134,7 +152,7 @@ export LISTEN_PORT=6000
export NGINX_PORT=6000
export NEMO_SKILLS_SANDBOX_PORT=6000
export SANDBOX_CONTAINER
export SANDBOX_COMMAND="/start-with-nginx.sh"
export SANDBOX_COMMAND="${SANDBOX_COMMAND:-/start-with-nginx.sh}"
export SANDBOX_ENV_VARS="NEMO_SKILLS_SANDBOX_PORT=${NEMO_SKILLS_SANDBOX_PORT}"

# ---- Build the run command ----
Expand All @@ -160,8 +178,8 @@ export COMMAND="export HF_MODULES_CACHE=${HF_MODULES_CACHE_DIR} ; \
PYTHONPATH=${SNAPSHOT_DIR}:\${PYTHONPATH:-} \
python ./examples/nemo_gym/run_grpo_nemo_gym.py \
--config ${CONFIG_PATH} \
env.nemo_gym.uv_venv_dir=${GYM_VENV_DIR} \
env.nemo_gym.skip_venv_if_present=true \
++env.nemo_gym.uv_venv_dir=${GYM_VENV_DIR} \
++env.nemo_gym.skip_venv_if_present=true \
policy.model_name=${MODEL_PATH} \
checkpointing.checkpoint_dir=${CHECKPOINT_DIR} \
logger.log_dir=${LOG_DIR} \
Expand Down Expand Up @@ -216,11 +234,27 @@ SBATCH_CMD=(
--job-name="${WANDB_NAME}"
--partition="${SLURM_PARTITION}"
--time="${SLURM_TIME_LIMIT}"
--gres=gpu:8
--gres=gpu:"${GPUS_PER_NODE:-8}"
--exclusive
--dependency=singleton
--dependency=singleton${SLURM_EXTRA_DEPENDENCY:+,${SLURM_EXTRA_DEPENDENCY}}
ray.sub
)
if [[ -n "${SLURM_QOS:-}" ]]; then
SBATCH_CMD=("${SBATCH_CMD[@]:0:1}" --qos="${SLURM_QOS}" "${SBATCH_CMD[@]:1}")
fi
if [[ -n "${SLURM_COMMENT:-}" ]]; then
SBATCH_CMD=("${SBATCH_CMD[@]:0:1}" --comment="${SLURM_COMMENT}" "${SBATCH_CMD[@]:1}")
fi
if [[ -n "${SLURM_LOG_DIR}" ]]; then
SBATCH_CMD=("${SBATCH_CMD[@]:0:1}" --output="${SLURM_LOG_DIR}/%j.out" --error="${SLURM_LOG_DIR}/%j.err" "${SBATCH_CMD[@]:1}")
fi
if [[ -n "${SLURM_SEGMENT:-}" ]]; then
if (( SBATCH_NUM_NODES % SLURM_SEGMENT != 0 )); then
echo "Error: SBATCH_NUM_NODES=${SBATCH_NUM_NODES} not divisible by SLURM_SEGMENT=${SLURM_SEGMENT}" >&2
exit 1
fi
SBATCH_CMD=("${SBATCH_CMD[@]:0:1}" --segment="${SLURM_SEGMENT}" "${SBATCH_CMD[@]:1}")
fi

if [[ "$DRY_RUN" == true ]]; then
echo ""
Expand Down
1 change: 1 addition & 0 deletions nemo_rl/algorithms/async_utils/trajectory_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ async def _iter_rollout_groups(
max_rollout_turns=None,
greedy=False,
reward_penalty_config=self.master_config.reward_penalties,
length_penalty_config=self.master_config.grpo.model_dump(),
thinking_tags=get_nemo_gym_thinking_tags(self.master_config.env),
mask_env_flagged_samples=should_mask_flagged_samples(
self.master_config.env
Expand Down
6 changes: 6 additions & 0 deletions nemo_rl/algorithms/grpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3015,6 +3015,7 @@ def grpo_train(
greedy=False,
effort_config=_get_effort_config(master_config),
reward_penalty_config=master_config.reward_penalties,
length_penalty_config=master_config.grpo.model_dump(),
thinking_tags=get_nemo_gym_thinking_tags(master_config.env),
mask_env_flagged_samples=should_mask_flagged_samples(
master_config.env
Expand Down Expand Up @@ -3938,6 +3939,11 @@ def validate(
greedy=False,
effort_config=_get_effort_config(master_config),
reward_penalty_config=master_config.reward_penalties,
# No length_penalty_config here: validation metrics
# (accuracy/pass_k) must reflect the raw env reward, and the
# adjustment code groups by the TRAINING stride
# (num_generations_per_prompt), which does not match
# val_num_generations_per_prompt.
thinking_tags=get_nemo_gym_thinking_tags(master_config.env),
mask_env_flagged_samples=should_mask_flagged_samples(
master_config.env
Expand Down
28 changes: 28 additions & 0 deletions nemo_rl/experience/rollouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
GenerationOutputSpec,
GenerationSamplingParams,
)
from nemo_rl.utils.length_penalty import apply_group_length_penalties
from nemo_rl.utils.multimodal_payload_metrics import (
collect_multimodal_payload_metrics,
print_multimodal_payload_metrics,
Expand Down Expand Up @@ -2271,6 +2272,7 @@ async def run_async_nemo_gym_rollout(
greedy: bool = False,
effort_config: Optional[EffortLevelsConfig] = None,
reward_penalty_config: dict[str, Any] | BaseModel | None = None,
length_penalty_config: dict[str, Any] | BaseModel | None = None,
thinking_tags: list[str] | tuple[str, ...] | None = None,
mask_env_flagged_samples: bool = True,
returns_entire_batch: bool = False,
Expand Down Expand Up @@ -2302,6 +2304,7 @@ async def run_async_nemo_gym_rollout(
greedy: Must be ``False`` because this path does not support greedy mode.
effort_config: Optional configuration for effort-based reward shaping.
reward_penalty_config: Optional reward-penalty configuration.
length_penalty_config: Optional GRPO config block for length adjustments.
thinking_tags: Optional opening and closing tags used by thinking penalties.
mask_env_flagged_samples: Whether to carry env-driven ``mask_sample``
flags in the rollout batch for loss masking.
Expand Down Expand Up @@ -2477,6 +2480,7 @@ async def run_async_nemo_gym_rollout(
log_full_result_tables=log_full_result_tables,
effort_config=effort_config,
reward_penalty_config=reward_penalty_config,
length_penalty_config=length_penalty_config,
thinking_tags=thinking_tags,
mask_env_flagged_samples=mask_env_flagged_samples,
)
Expand Down Expand Up @@ -2514,6 +2518,7 @@ def run_nemo_gym_rollout_sync(
greedy: bool = False,
effort_config: Optional[EffortLevelsConfig] = None,
reward_penalty_config: dict[str, Any] | BaseModel | None = None,
length_penalty_config: dict[str, Any] | BaseModel | None = None,
thinking_tags: list[str] | tuple[str, ...] | None = None,
sampling_params: Optional[GenerationSamplingParams] = None,
mask_env_flagged_samples: bool = True,
Expand Down Expand Up @@ -2578,6 +2583,7 @@ async def _consume_rollout() -> NemoGymRolloutResult:
greedy=greedy,
effort_config=effort_config,
reward_penalty_config=reward_penalty_config,
length_penalty_config=length_penalty_config,
thinking_tags=thinking_tags,
mask_env_flagged_samples=mask_env_flagged_samples,
returns_entire_batch=True,
Expand All @@ -2604,6 +2610,7 @@ def _postprocess_single_nemo_gym_group(
log_full_result_tables: bool,
effort_config: Optional[EffortLevelsConfig] = None,
reward_penalty_config: dict[str, Any] | BaseModel | None = None,
length_penalty_config: dict[str, Any] | BaseModel | None = None,
thinking_tags: list[str] | tuple[str, ...] | None = None,
mask_env_flagged_samples: bool = True,
) -> NemoGymRolloutResult:
Expand All @@ -2620,6 +2627,27 @@ def _postprocess_single_nemo_gym_group(
)
penalty_counts = apply_reward_penalties(results, resolved_reward_penalty_config)

if length_penalty_config is not None:
grpo_config = (
length_penalty_config.model_dump()
if isinstance(length_penalty_config, BaseModel)
else dict(length_penalty_config)
)
# Callers pass the whole grpo config block; runs without a
# grpo.length_penalty section are untouched by this block.
if grpo_config.get("length_penalty"):
# Copy the per-row fields the length adjustments consume.
for nemo_gym_row, result in zip(nemo_gym_rows, results):
result["agent_ref"] = nemo_gym_row["agent_ref"]
result["profiled_rewards"] = nemo_gym_row.get("profiled_rewards")
result["profiled_output_lengths"] = nemo_gym_row.get(
"profiled_output_lengths"
)
result["profile_band"] = nemo_gym_row.get("profile_band")
apply_group_length_penalties(
results, {"grpo": grpo_config}, tokenizer=tokenizer
)

# Prepare for the rollout metrics calculation below. Not strictly necessary here, but good to have parity with `run_async_multi_turn_rollout`
with timer.time(f"{timer_prefix}/prepare_for_metrics_calculation"):
batch_size = len(nemo_gym_rows)
Expand Down
Loading
Loading