Generative AI

NeMo RL ではじめる強化学習 – Math 編

Reading Time: 6 minutes

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.0nvcr.io/nvidia/nemo-rl:v0.6.0
v0.5.0nvcr.io/nvidia/nemo-rl:v0.5.0
nano-v3nvcr.io/nvidia/nemo-rl:v0.4.0.nemotron_3_nano
super-v3nvcr.io/nvidia/nemo-rl:v0.5.0.nemotron_3_super
表 1. NeMo RL のリポジトリとコンテナー イメージの対応

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_step512641 step の実行時間短縮
max_total_sequence_length204808192計算時間/メモリ削減
training backendDTensorMegatron計算効率の改善
generation backendvLLMvLLM
mamba_ssm_cache_dtypefloat32Nemotron Nano 2/3 特有の設定
gpu memory utilization0.60.85ロールアウトのパフォーマンス改善
表 2. Configの主な変更点 (アルゴリズム特有の設定を除く)

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: true
      • batch_multiplier: 2
      • dynamic_sampling_max_gen_batches: 10
  • Overlong Filtering
    • 通常、生成するサンプルの最大長を設定し、長すぎるサンプルは切り捨てられ、ペナルティ報酬が割り当てられる。このアプローチでは健全な推論プロセスが単に長すぎるという理由だけでペナルティを受ける可能性があることから、切り捨てられたサンプルの損失をマスクする。
    • 変更: overlong_filtering: true
  • Soft Overlong Punishment
    • 応答の長さが事前に定義された最大値を超えた場合のペナルティ区間を定義。この区間内では応答が長くなるほど、受けるペナルティが大きくなる。このペナルティは元の正誤報酬に加算され、モデルに対して過度に長い応答を避けるように促す。
    • 変更: 今回のチュートリアルでは生成長を8kへ短くしているため、適用しませんでした。reward_scalingreward_shapingtrue に変更すると論文と同様の設定を適用できます。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-JapaneseNVIDIA-Nemotron-Nano-9B-v2-Japanese + DAPO 260 steps checkpoint
MCLM MATH-100 (日本語) Pass@10.9360.946
MATH-5000.9560.968
PolyMath (日本語) Low0.8640.88
PolyMath (日本語) Medium0.6720.696
PolyMath (日本語) High0.560.56
PolyMath (日本語) Top0.2720.32
PolyMath (日本語) Average0.5920.614
AIME 24 Pass@10.6660.791
AIME 25 Pass@10.5410.7
表 3. 学習前後の数学ベンチマークのスコア

モデルの学習を大規模にスケールする前に考慮すべき点

ここでは本記事を執筆するにあたり、得た教訓を紹介します。

学習バックエンドと生成バックエンドの対数確率の一貫性を確認する

新しいモデルを試す際は、ドキュメントにある 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 を使用して強化学習を試していただけると嬉しいです。

関連情報

Tags