NeMo RL とは
NeMo RL は、NVIDIA がオープン ソースとして公開しているマルチモーダル モデル (LLM、VLM など) 向けの事後学習ライブラリです。教師ありファインチューニング (SFT: Supervised Fine-Tuning) や強化学習 (RL: Reinforcement Learning) などの事後学習を効率的、かつスケーラブルに実行できるよう設計されており、小規模な実験からマルチ GPU、マルチノードでの実行まで対応します。また、サンプル Config や実行スクリプトが豊富なため、学習をすぐに開始することができます。
NeMo RL は、さまざまなモデル サイズやハードウェア構成に対応するために、複数の学習/生成バックエンドをサポートしています。
学習バックエンド
- DTensor: PyTorch の次世代分散トレーニング。メモリ効率が向上 (PyTorch ネイティブの TP、SP、PP、CP、および FSDP2 に対応)。
- Megatron: NVIDIA の高性能トレーニング フレームワーク。6D 並列処理により大規模モデルにも対応可能。
生成バックエンド
- vLLM: 高スループットかつメモリ効率に優れた、人気の高い推論およびサービス提供エンジン。
- Megatron: 学習と推論間の重み変換を排除する、高性能な Megatron ネイティブ推論バックエンド。
- SGLang: v0.6.0 から SGLang のサポートを追加。現在は DTensor v2 (AutoModel) バックエンドのみがサポートされています。詳細はこちらを参照してください。
現在 (v0.6.0) の NeMo RL は、以下の事後学習手法をサポートしています。
- Supervised Fine-Tuning (SFT): 教師ありデータによるファインチューニング
- Direct Preference Optimization (DPO): 人間の好みや比較データを用いた最適化
- Reinforcement learning (RL): GRPO、GSPO、DAPO、ProRLv2、GDPO などのポリシー最適化手法
- Reward Modeling: 報酬モデルの学習
NeMo RL は GitHub 上にあるリポジトリ と NGC 上にある NeMo RL のコンテナーを使用することですぐに始めることができます。
執筆時点での最新 NeMo RL は v0.6.0 ですが、本チュートリアルでは NVIDIA Nemotron 3 Nano 向けに検証済みの nano-v3 ブランチおよび v0.4.0.nemotron_3_nano コンテナーを使用します。そのため、一部の機能/設定名は v0.6.0 とは異なる可能性があります。
本記事のゴールは、NeMo RL を使って Nemotron Nano 9B v2 Japanese に DAPO を適用する最小限の実行例を示し、さらに実運用および大規模化の前に確認すべき観点を共有します。
NeMo RL チュートリアル
本記事では、NeMo RL であらかじめ用意されている数学データセットと環境で、GRPO の派生アルゴリズムである DAPO を実行します。データ、スクリプトはサンプルとして用意されているため、実行自体は簡単です。まずは実行手順を示し、その後に大規模な学習を始める前に事前に考慮すべき点を解説します。
- モデル:
nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese - 学習バックエンド: Megatron
- 生成バックエンド: vLLM
- RLアルゴリズム: DAPO (一部改変)
- タスク: 数学 (DAPOMath17K データセット)
- 同期学習 + コロケーション (学習と生成で同じ GPU を使用する)
今回のチュートリアルの検証環境は以下の条件で行っております。こちらの構成は一例であり、NeMo RL は単一 GPU からマルチノード環境で実行することが可能です。
- ハードウェア
- NVIDIA DGX H200
- GPU: 8 x NVIDIA H200 141 GB GPU (driver version: 580.126.09)
- CPU: Intel Xeon Platinum 8480C
- システム メモリ: 2 TB
- ソフトウェア
- OS: Ubuntu 24.04.3 LTS
- Container:
nvcr.io/nvidia/nemo-rl:v0.4.0.nemotron_3_nano
事前準備
以下のコマンドで作業用のディレクトリを作成し、移動します。
mkdir rl-example
cd rl-example
リポジトリの Clone
以下のコマンドを実行し、NeMo RL の実行環境を整えます。今回は後述する学習/生成バックエンド間の対数確率誤差が小さかった nano-v3 ブランチを使用しています。
git clone --branch nano-v3 --recursive git@github.com:NVIDIA-NeMo/RL.git nemo-rl-nano-v3
cd nemo-rl-nano-v3
uv venv
コンテナーの準備
このチュートリアルでは NGC 上にある NeMo RL のコンテナーを使用します。ベアメタル (コンテナー外) で使用する場合は、こちらを参照して別途環境構築してください。
docker pull nvcr.io/nvidia/nemo-rl:v0.4.0.nemotron_3_nano
リポジトリと NGC コンテナー イメージの tag を揃えておかないと実行時にエラーや不具合が発生する可能性があります。
| リポジトリ ブランチ/タグ | コンテナー イメージ タグ |
|---|---|
| v0.6.0 | nvcr.io/nvidia/nemo-rl:v0.6.0 |
| v0.5.0 | nvcr.io/nvidia/nemo-rl:v0.5.0 |
| nano-v3 | nvcr.io/nvidia/nemo-rl:v0.4.0.nemotron_3_nano |
| super-v3 | nvcr.io/nvidia/nemo-rl:v0.5.0.nemotron_3_super |
Docker コンテナーの起動
以下のコマンドでコンテナーを起動します。
docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \
-v $PWD:$PWD \
-w $PWD \
-it nvcr.io/nvidia/nemo-rl:v0.4.0.nemotron_3_nano bash
Config ファイルの作成
DAPO で数学タスクを実行するため、yaml ベースの Config ファイルを用意します。ベースはリポジトリ内の example にある dapo-qwen2.5-7b.yaml とそのベースとなっている grpo_math_1B.yaml を参照しながら以下の変更を適用しています。
- Megatron 学習バックエンドの有効化
- Nemotron Nano 2/3 特有の設定 (
mamba_ssm_cache_dtype: "float32") を追加 - リソース、計算時間の兼ね合いから 1 ステップでのプロンプト数を 512 から 64 へ変更
- 1 step の学習では、後述の Dynamic Sampling を加味しない場合、64 のプロンプトに対して、それぞれ 16 本の生成が行われ、合計 1,024 のサンプルが生成されます。
- 同様にリソース、計算時間の兼ね合いから生成時の最大生成長を原論文の 20k から 8k へ変更
この yaml ファイルを examples/configs の下に custom_dapo-nanov2-9b.yaml という名前で配置します。今回は examples/configs の下を選びましたが特に指定はありません。
実行するリポジトリのバージョンによって yaml ファイルで使われているキーの名前に微妙な差があったり、新たなキーが追加されている場合があります。使用するバージョンの Config ファイルを参考にしてください。
grpo:
num_prompts_per_step: 64 # 1stepに時間がかかりすぎたため、512から64に変更
num_generations_per_prompt: 16
max_rollout_turns: 1 # for multi-turn rollouts. Math Environments just have 1 turn (answering the question)
max_num_epochs: 1
max_num_steps: 10000
normalize_rewards: true
use_leave_one_out_baseline: false
val_period: 5
val_at_start: false
overlong_filtering: true # false -> trueへ変更。生成長が長すぎるものはフィルタリング
max_val_samples: 960
val_batch_size: 960
seed: 42
use_dynamic_sampling: true
batch_multiplier: 2 # Multiplier for dataloader batch size calculation (batch_multiplier × num_prompts_per_step). Following DAPO dynamic sampling, the actual training batch size equals num_prompts_per_step × num_generations_per_prompt.
dynamic_sampling_max_gen_batches: 10
reward_scaling:
enabled: false # true -> falseへ変更。報酬をスケーリングしない
source_min: 0.0
source_max: 1.0
target_min: -1.0
target_max: 1.0
reward_shaping:
enabled: false # ソフトペナルティを設定する場合はTrue 例.16k + 4k (overlong_buffer_length) 生成が16kを超える場合、徐々に報酬を減らす
overlong_buffer_length: 4096
overlong_buffer_penalty: 1
max_response_length: 20480
async_grpo:
enabled: false # Set to true to enable async training mode
# Max age (in training steps) for trajectories used in training
max_trajectory_age_steps: 1
in_flight_weight_updates: false # Set to true to enable in-flight weight updates
recompute_kv_cache_after_weight_updates: false # Set to true to recompute kv cache after in-flight-weight-updates
seq_logprob_error_threshold: null
loss_fn:
reference_policy_kl_penalty: 0.0
# Can be set to k1, k2, k3
# For more details, see http://joschu.net/blog/kl-approx.html
reference_policy_kl_type: "k3"
kl_input_clamp_value: null
kl_output_clamp_value: null
ratio_clip_min: 0.2
ratio_clip_max: 0.28
ratio_clip_c: null
# (default off) loss formulation improvements (docs/guides/grpo.md#loss)
use_on_policy_kl_approximation: false
# Async GRPO requires importance sampling correction enabled
# Set to true when async_grpo.enabled is true
use_importance_sampling_correction: false
truncated_importance_sampling_ratio: null
sequence_level_importance_ratios: false
token_level_loss: true
force_on_policy_ratio: false # Set to true to force ratio=1.0 (requires train_global_batch_size == num_prompts_per_step * num_generations_per_prompt)
checkpointing:
enabled: true
checkpoint_dir: results/dapo-nanov2-9b
metric_name: "val:accuracy" # one of "val:" or "train:" followed by the metric name
higher_is_better: true
keep_top_k: 5
save_period: 5
checkpoint_must_save_by: null
save_optimizer: true
policy:
model_name: nvidia/NVIDIA-Nemotron-Nano-9B-v2-Japanese
tokenizer:
name: ${policy.model_name} ## specify if you'd like to use a tokenizer different from the model's default
train_global_batch_size: ${mul:${grpo.num_prompts_per_step}, ${grpo.num_generations_per_prompt}}
train_micro_batch_size: 1
generation_batch_size: 32 # Only used when generating using HF backend
logprob_batch_size: 1
max_total_sequence_length: 8192 # 今回の検証では8192に設定
precision: "bfloat16"
logprob_chunk_size: 2048
dtensor_cfg:
_v2: false
enabled: false
cpu_offload: False
sequence_parallel: false
activation_checkpointing: false
tensor_parallel_size: 1
context_parallel_size: null
megatron_cfg:
enabled: true
empty_unused_memory_level: 1
activation_checkpointing: false
bias_activation_fusion: false
tensor_model_parallel_size: 2
expert_tensor_parallel_size: 1
expert_model_parallel_size: 1
pipeline_model_parallel_size: 1
num_layers_in_first_pipeline_stage: null
num_layers_in_last_pipeline_stage: null
context_parallel_size: 1
pipeline_dtype: ${policy.precision}
sequence_parallel: true
freeze_moe_router: true
moe_router_dtype: "fp32"
moe_router_load_balancing_type: "none" # "seq_aux_loss" causes logprob error divergence for grpo
moe_router_bias_update_rate: 1e-3
moe_permute_fusion: true
moe_enable_deepep: false
moe_token_dispatcher_type: "alltoall"
moe_aux_loss_coeff: 0.0
moe_router_enable_expert_bias: true
#gives ~20% training perf speedup with sequence packing
apply_rope_fusion: True
defer_fp32_logits: True
track_moe_metrics: True
moe_per_layer_logging: True
moe_shared_expert_overlap: false
gradient_accumulation_fusion: false
optimizer:
optimizer: "adam"
lr: 1.0e-06
min_lr: 1.0e-06
weight_decay: 0.0
bf16: true
fp16: false
params_dtype: "float32"
#adam
adam_beta1: 0.9
adam_beta2: 0.999
adam_eps: 1e-8
#sgd
sgd_momentum: 0.9
clip_grad: ${policy.max_grad_norm}
#distributed optimizer
use_distributed_optimizer: true
use_precision_aware_optimizer: true
# optimizer cpu offload
optimizer_cpu_offload: false
optimizer_offload_fraction: 0.0
scheduler:
start_weight_decay: ${policy.megatron_cfg.optimizer.weight_decay}
end_weight_decay: ${policy.megatron_cfg.optimizer.weight_decay}
weight_decay_incr_style: "constant"
lr_decay_style: "constant"
lr_decay_iters: null
lr_warmup_iters: 10
lr_warmup_init: 1.0e-07
distributed_data_parallel_config:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: false
use_custom_fsdp: false
data_parallel_sharding_strategy: "optim_grads_params"
env_vars:
PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:True"
NCCL_DEBUG: "WARN"
CUDA_DEVICE_MAX_CONNECTIONS: "1"
dynamic_batching:
enabled: False
train_mb_tokens: ${mul:${policy.max_total_sequence_length}, ${policy.train_micro_batch_size}}
logprob_mb_tokens: ${mul:${policy.max_total_sequence_length}, ${policy.logprob_batch_size}}
sequence_length_round: 64
sequence_packing:
enabled: true
train_mb_tokens: ${mul:${policy.max_total_sequence_length}, ${policy.train_micro_batch_size}}
logprob_mb_tokens: ${mul:${policy.max_total_sequence_length}, ${policy.logprob_batch_size}}
algorithm: "modified_first_fit_decreasing"
sequence_length_round: 64
make_sequence_length_divisible_by: ${policy.dtensor_cfg.tensor_parallel_size}
max_grad_norm: 1.0
optimizer: null # remove default FSDP optimizer
scheduler: null
offload_optimizer_for_logprob: false # Only useful for non-colocated generation since colocated generation will always offload optimizer to cuda before refit
generation:
backend: "vllm"
max_new_tokens: ${policy.max_total_sequence_length}
temperature: 1.0
top_p: 1.0
top_k: null
stop_token_ids: null
stop_strings: null
vllm_cfg:
# NB: can re-enable prefix cache on vllm >= 0.11.2.
# enable_prefix_caching: false
async_engine: false
kv_cache_dtype: auto
precision: ${policy.precision}
tensor_parallel_size: 1
pipeline_parallel_size: 1
expert_parallel_size: 1 # When EP > 1, EP must be a multiple of TP since vLLM's EP = DP * TP
gpu_memory_utilization: 0.85 # 0.6から0.85に変更
max_model_len: ${policy.max_total_sequence_length}
# when enforce_eager is False, it is optional to set ++policy.generation.vllm_kwargs.compilation_config.backend=eager for better accuracy,
# with the flag, vllm will use the custom CUDA kernels instead of the Triton kernels generated by torch.compile
# for more details, see convergence issue https://github.com/NVIDIA-NeMo/RL/issues/998
enforce_eager: False
use_deep_gemm: False
num_last_layers_in_bf16: 0
num_first_layers_in_bf16: 0
expose_http_server: true
http_server_serving_chat_kwargs:
enable_auto_tools: true
vllm_kwargs:
mamba_ssm_cache_dtype: "float32" # mambaベースのnemotron用
compilation_config:
# when enforce_eager is False, set ++policy.generation.vllm_kwargs.compilation_config.backend=eager for better accuracy,
# with the flag, vllm will use the custom CUDA kernels instead of the Triton kernels generated by torch.compile
# for more details, see convergence issue https://github.com/NVIDIA-NeMo/RL/issues/998
backend: eager
colocated:
# true: generation shares training GPUs
# false: uses dedicated generation resources
enabled: true
# only relevant when enabled is false
resources:
gpus_per_node: null # Decides num gpus to be dedicated to generation when there is one node in the cluster i.e cluster.num_nodes == 1
num_nodes: null # Decides number of nodes to be dedicated to generation
data:
max_input_seq_length: 2048
prompt_file: null
system_prompt_file: null
shuffle: true
num_workers: 1
dataset_name: DAPOMath17K
env:
math:
num_workers: 8
math_verify_impl: "dapo_math_verify"
logger:
log_dir: "logs/dapo-nanov2-9b" # Base directory for all logs
num_val_samples_to_print: 0 # Number of validation samples to pretty print on terminal
wandb_enabled: true
tensorboard_enabled: false
mlflow_enabled: false # Disable MLflow logging
swanlab_enabled: false # Disable SwanLab logging
monitor_gpus: true # If true, will monitor GPU usage and log to wandb and/or tensorboard
wandb:
project: "grpo-dev"
name: "dapo-nanov2-9b-dev-logger"
tensorboard: {}
mlflow:
experiment_name: "grpo-dev"
run_name: "grpo-dev-logger"
gpu_monitoring:
collection_interval: 10 # How often to collect GPU usage metrics (in seconds)
flush_interval: 10 # How often to flush GPU usage metrics to the loggers (in seconds)
cluster:
gpus_per_node: 8
num_nodes: 1
今回のチュートリアルで使用した主要な Config の変更点 (アルゴリズム特有の設定を除く) を示します。なお、本設定は NeMo RL 上で Nemotron Nano 9B v2 Japanese を用いて RL 学習を実行するための一例です。モデル、GPU 構成、タスクによって最適な値は変わるため、大規模な学習を行う前には個別に確認することを推奨します。
| 項目 | 元設定 / 参考設定 | 本記事の設定 | 理由 |
|---|---|---|---|
| num_prompts_per_step | 512 | 64 | 1 step の実行時間短縮 |
| max_total_sequence_length | 20480 | 8192 | 計算時間/メモリ削減 |
| training backend | DTensor | Megatron | 計算効率の改善 |
| generation backend | vLLM | vLLM | |
| mamba_ssm_cache_dtype | – | float32 | Nemotron Nano 2/3 特有の設定 |
| gpu memory utilization | 0.6 | 0.85 | ロールアウトのパフォーマンス改善 |
DAPOとは
Config ファイルを作成したので、ここでは今回、使用するアルゴリズムである DAPO の解説をします (GRPO の解説はここでは省略します。GRPO は「DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models」が元論文で日本語でも多くの解説記事が出ています)。
DAPO をはじめ、GRPO の派生アルゴリズムは GRPO のベース Config である grpo_math_1B.yaml を変更して実行できます (実際のところ、grpo_math_1B.yaml 内の設定は grpo という名前がついているものの、一部は DAPO 的な設定になっています)。リポジトリ内の examples/configs には DAPO や GSPO、v0.6.0 以降では ProRLv2、GDPO などの config 例があるので参照してください。
DAPO は標準の GRPO を安定、効率化させるために以下の変更を適用した手法です。
- KL ペナルティ項の除去
- 長い CoT リーズニング モデルの学習では、モデルの分布は初期モデルから大きく逸脱する可能性があるため、この制約は必要ないと主張。
- 変更:
reference_policy_kl_penalty: 0.0
- Clip-Higher
- 学習が進むにつれて出力の多様性が失われるエントロピー崩壊を防ぐために重要度サンプリング比の上限を引き上げて探索空間を広げる。
- 変更:
ratio_clip_max: 0.2 -> 0.28(デフォルトの grpo config も 0.28 を使用)
- Token-level Loss
- オリジナルの GRPO はサンプル レベルで損失を平均化して集計するため、より長い応答内のトークンは損失全体への寄与が不釣り合いに低くなる可能性がある。バッチ全体の有効トークン数で平均化する。
- 変更:
token_level_loss: true(デフォルトの grpo config も true を使用)
- Dynamic Sampling
- プロンプト内の生成グループの回答が全て正解/不正解の場合に勾配が 0 になり、バッチ全体の勾配も小さくなり、サンプル効率が低下する。これを防ぐために該当するプロンプトをフィルタリングして再サンプリングを行い、有効なプロンプトを一定に保つ。
- 変更:
use_dynamic_sampling: truebatch_multiplier: 2dynamic_sampling_max_gen_batches: 10
- Overlong Filtering
- 通常、生成するサンプルの最大長を設定し、長すぎるサンプルは切り捨てられ、ペナルティ報酬が割り当てられる。このアプローチでは健全な推論プロセスが単に長すぎるという理由だけでペナルティを受ける可能性があることから、切り捨てられたサンプルの損失をマスクする。
- 変更:
overlong_filtering: true
- Soft Overlong Punishment
- 応答の長さが事前に定義された最大値を超えた場合のペナルティ区間を定義。この区間内では応答が長くなるほど、受けるペナルティが大きくなる。このペナルティは元の正誤報酬に加算され、モデルに対して過度に長い応答を避けるように促す。
- 変更: 今回のチュートリアルでは生成長を8kへ短くしているため、適用しませんでした。
reward_scalingとreward_shapingをtrueに変更すると論文と同様の設定を適用できます。reward_shapingは論文 (Config) の設定では 16k を最大生成長として期待し、それを超える場合にバッファ期間 (4k) を設け、報酬を線形に減衰させます。
Dynamic Sampling を適用する場合、今回の設定では 64 × batch_multiplier: 2 (今回の設定) の合計 128 のプロンプトに対して、16 本の生成が行われ、合計 2048 のサンプルを生成します。ここから、全て正解/不正解のプロンプトをフィルタリングし、1024 サンプルが確保できれば、学習に進みます。足りなければ新たに 128 プロンプトを追加で生成 (2048 サンプル) し、前回のものに加えて 1024 サンプル確保できるかどうか確認します。足りなければ最大 dynamic_sampling_max_gen_batches 回数 (今回は 10 に設定) 処理が繰り返されます。
今回の設定では学習が進むと徐々にフィルタリングされるプロンプトが増え、40 steps を超えたあたりから、サンプル確保にほぼ 2 回の実行が必要となりました。全体の処理時間を効率化することを踏まえると batch_multiplier は 3 に設定した方が良かったかもしれません。

DAPO の実行
以下のコマンドで DAPO を実行します。WANDB で学習ログを記録したい場合は WANDB_API_KEY を環境変数に追加してください。学習プロセスで収集されるメトリクスがとても多いため、利用を推奨します。
export WANDB_API_KEY="YOUR-WANDB-API-KEY"
uv run examples/run_grpo_math.py --config examples/configs/custom_dapo-nanov2-9b.yaml
以下が 300 steps 実行した際の報酬の遷移と検証データセット (AIME 24) での精度の遷移になります。計算リソースの都合上、ジョブは 4 回に分けて実行されています。


学習が終わったチェックポイントはリポジトリ内の変換スクリプトで HF 形式へ変換できます。評価には、300 steps の学習中で validation accuracy が最も高かった 260 steps checkpoint を使用しました。今回使用したバージョンでは以下になります。
CKPT_DIR=results/dapo-nanov2-9b/step_260
uv run --extra mcore examples/converters/convert_megatron_to_hf.py --config=$CKPT_DIR/config.yaml --megatron-ckpt-path=$CKPT_DIR/policy/weights/iter_0000000/ --hf-ckpt-path=output/NVIDIA-Nemotron-Nano-9B-v2-Japanese-DAPO-step_260
学習バックエンドによって、使用するスクリプトが異なる点、リポジトリのバージョンによって引数が微妙に異なることがある点に気をつけてください。詳細は対応するバージョンのドキュメント内のセクションを参照してください。
学習後のモデルの評価
学習前後のモデルを swallow-evaluation-instruct Ver. 202604 内の数学タスクで評価しました。最大コンテキスト長は 32,768 トークン、生成パラメーターは temperature = 0.6、top-p = 0.95 を使用しています。
結果を見ると数学タスクは日本語/英語ベンチマーク共に概ね改善しており、なかでも AIME 24/25 の改善幅が大きかったです。
これは DAPO-Math-17k データセットが AIME にインスパイアされて構築されたデータセットであることに関連しているかもしれません。AIME 24/25 は、2024 年と 2025 年の American Invitational Mathematics Examination の問題で構成される数学能力を問うベンチマークで回答は整数になります。DAPO-Math-17k データセットも同様に答えが整数となる/整数に変換できた問題を対象に選別されています。
また、モデルの評価には、学習時の検証データセットであった AIME 24 が最大となった checkpoint で評価していることも影響している可能性があります。
| NVIDIA-Nemotron-Nano-9B-v2-Japanese | NVIDIA-Nemotron-Nano-9B-v2-Japanese + DAPO 260 steps checkpoint | |
|---|---|---|
| MCLM MATH-100 (日本語) Pass@1 | 0.936 | 0.946 |
| MATH-500 | 0.956 | 0.968 |
| PolyMath (日本語) Low | 0.864 | 0.88 |
| PolyMath (日本語) Medium | 0.672 | 0.696 |
| PolyMath (日本語) High | 0.56 | 0.56 |
| PolyMath (日本語) Top | 0.272 | 0.32 |
| PolyMath (日本語) Average | 0.592 | 0.614 |
| AIME 24 Pass@1 | 0.666 | 0.791 |
| AIME 25 Pass@1 | 0.541 | 0.7 |
モデルの学習を大規模にスケールする前に考慮すべき点
ここでは本記事を執筆するにあたり、得た教訓を紹介します。
学習バックエンドと生成バックエンドの対数確率の一貫性を確認する
新しいモデルを試す際は、ドキュメントにある Add New Models セクションを参照することをお勧めします。ここにあるように、学習バックエンドと生成バックエンドが異なることによって、さまざまな要因で対数確率の不一致が生じます。これが著しく異なる場合、オンポリシー学習でありながら、実質的に異なる分布 (モデル) からサンプリングしていることになり、損失の推定に誤差が生じます。大規模な学習を始める前には学習と生成バックエンド間で対数確率誤差がどの程度生じているか確認することを推奨します。学習中の誤差は wandb 上では train/token_mult_prob_error で確認でき、NeMo RL のドキュメントでは、1.05 を超える場合は調査が必要としています。この値は各 Step で一定とは限らず、場合によってはドリフトして大きくなってしまったり、急なスパイクが発生することもあります。

今回のチュートリアルでの予備実験で、誤差は学習バックエンドと生成バックエンドの単純な違いだけではなく、モデルやデータ、Config、各バックエンドのバージョンの違いによっても変わってくることを確認しました。少なくとも今回の設定では、v0.6.0よりも nano-v3 リポ + イメージを使用する方が誤差は小さく、安定していました (他のバージョンは試していません)。
生成時間の最適化を検討する
強化学習では生成プロセスの時間が学習全体に占める割合が大きくなります。今回の設定でも Step により変動はあるものの、学習ログから確認できる範囲で 60-70 % 以上が生成プロセスに費やされていました。生成の効率化のために、モデル サイズや最大生成長、ハードウェアに合わせて、生成バックエンドのモデル並列化や gpu_memory_utilization などを複数の設定で事前に試すことをお勧めします。
また、十分な計算リソースが利用できる場合は非同期学習の適用 (async_grpo true + 非コロケーション) も検討できます。非同期によって学習が不安定になる可能性もありますので十分な事前調査をお勧めします。
まとめ
本記事では、NeMo RL を使用した DAPO (GRPO の派生アルゴリズム) の実行を紹介しました。NeMo RL を使用して強化学習を試していただけると嬉しいです。
関連情報
- 関連ドキュメント: NeMo RL Documentation
- リポジトリ: GitHub – NVIDIA-NeMo/RL
- コンテナー イメージ: NGC – NVIDIA nemo-rl
- 技術ブログ: NVIDIA NeMo RL を用いた合成データによる Supervised Fine-Tuning (SFT)