From 0fa333cc07d829a79aa5c3a2b2b065350e0d4b1f Mon Sep 17 00:00:00 2001 From: slikhite-1 Date: Thu, 21 Aug 2025 10:10:01 -0700 Subject: [PATCH 1/3] Docs added for sliding puzzle example Signed-off-by: slikhite-1 --- docs/assets/train-reward-sliding-puzzle.png | Bin 0 -> 30308 bytes docs/assets/valid_acc-sliding-puzzle.png | Bin 0 -> 14401 bytes docs/guides/grpo-sliding-puzzle.md | 294 ++++++++++++++++++++ examples/configs/grpo_sliding_puzzle.yaml | 8 +- 4 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 docs/assets/train-reward-sliding-puzzle.png create mode 100644 docs/assets/valid_acc-sliding-puzzle.png create mode 100644 docs/guides/grpo-sliding-puzzle.md diff --git a/docs/assets/train-reward-sliding-puzzle.png b/docs/assets/train-reward-sliding-puzzle.png new file mode 100644 index 0000000000000000000000000000000000000000..82d319f4f23b4061a98dfb223e89787fb2ceb388 GIT binary patch literal 30308 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

VSA%2G;kB~EDSS3`kB8U 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 Wlf%4~0!@a6-Mu3xkt?R_{eJ*|!9%D3 literal 0 HcmV?d00001 diff --git a/docs/assets/valid_acc-sliding-puzzle.png b/docs/assets/valid_acc-sliding-puzzle.png new file mode 100644 index 0000000000000000000000000000000000000000..7b6d539916dab7b0242a3359acebd87fb45861da GIT binary patch literal 14401 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%0A;!F(g*#aAr3sG_MeQ(F-6 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 literal 0 HcmV?d00001 diff --git a/docs/guides/grpo-sliding-puzzle.md b/docs/guides/grpo-sliding-puzzle.md new file mode 100644 index 0000000000..8b69931fe8 --- /dev/null +++ b/docs/guides/grpo-sliding-puzzle.md @@ -0,0 +1,294 @@ +# Solve a Sliding Puzzle Using GRPO + +This guide explains how to use Nemo RL to train a model to solve the classic **nxn sliding puzzle** game through multi-turn reinforcement learning. This environment implements a classic **n×n sliding puzzle** where numbered tiles must be arranged in sequential order by sliding them into an empty space. + +The sliding puzzle task serves as a simple, yet effective example, to illustrate how multi-turn RL and tool-calling are implemented within Nemo RL. This example provides a minimal setup for understanding the core components of Group Relative Policy Optimization (GRPO) and sequential decision-making. + + +## Quick Start Guide + +#### 1. Install and Set Up NeMo RL with Megatron Backend (Optional) + +To get started, clone and set up the NeMo RL repository by initializing submodules, installing CUDA dependencies, and configuring the environment with uv. Refer to [Prerequisites](https://github.com/NVIDIA-NeMo/RL/tree/main?tab=readme-ov-file#prerequisites) for detailed instructions on installation. + +#### 2. Train a Model + +Train a model to solve the sliding puzzle using GRPO with the default 2×2 configuration. + +```bash +uv run python examples/run_grpo_sliding_puzzle.py +``` + +#### 3. Customize Puzzle Configuration + +By default, this training script uses the configuration in [grpo_sliding_puzzle.yaml](../../examples/configs/grpo_sliding_puzzle.yaml). You can customize parameters with command-line overrides to experiment with different puzzle sizes or levels of difficulty. +```bash +# Train on a 3×3 puzzle with 10 random moves to scramble the board +uv run python examples/run_grpo_sliding_puzzle.py \ + env.sliding_puzzle_game.cfg.game_config.size=3 \ + env.sliding_puzzle_game.cfg.game_config.shuffle_moves=10 +``` + +#### 4. Monitor Progress + +You can enable logging via Weights & Biases and TensorBoard to monitor training metrics such as rewards, success rate, and loss curves. + +```bash +# Enable logging (optional) +uv run examples/run_grpo_sliding_puzzle.py \ + --config examples/configs/grpo_sliding_puzzle.yaml \ + logger.wandb_enabled=true \ + logger.tensorboard_enabled=true +``` + +## Game Mechanics + +### Puzzle Structure + +The sliding puzzle consists of: +- **Grid**: An `n×n` grid with numbered tiles and one empty space +- **Tiles**: Numbered from `1` to `n²-1`, placed in random order +- **Empty Space**: Represented by `0`, typically starting at the bottom-right corner +- **Goal State**: Sequential arrangement `1, 2, 3, ..., n²-1` with `0` at bottom-right + +### Example Data Sample +``` +===== SLIDING PUZZLE ===== +Arrange the 3x3 grid by sliding tiles into the empty space. +- The goal is to arrange numbers from 1 to 8 in order +- Use 'up', 'down', 'left', 'right' to slide in that direction +- Use 'view' to see the current state of the board + +Current Board State: + + +---------+ +1 | 1 3 | +2 | 4 2 5 | +3 | 7 8 6 | + +---------+ + 1 2 3 + +Reach the goal state where numbers are ordered 1 through 8 with the empty space (0) at the bottom right. +Valid actions: 'up', 'down', 'left', 'right', or 'slide row col' (e.g., 'slide 1 2'). +After thinking, output your chosen action on a new line starting with '' like this: +your_action +If you just want to see the board, output view +Think carefully step-by-step before acting. + +``` + +### Movement Rules + +1. **Valid Moves**: Only tiles adjacent to the empty space `0` can be moved. +2. **Movement Direction**: Tiles slide into the empty space, not the other way around. +3. **Grid Boundaries**: Moves that would go beyond the grid are invalid. +4. **Single Tile Movement**: Each action affects only one tile at a time. + +All actions must be wrapped in XML-style tags and follow one of the formats below: +```xml +up +slide 2 1 +view +``` + +## Data Generation + +### Configuration Parameters + +Sliding puzzle instances are generated using the following parameters, which can be customized via the configuration file: + +```yaml +env: + sliding_puzzle_game: + cfg: + game_config: + size: 5 # Size of the puzzle grid (e.g., 3x3, 4x4, 5x5) + shuffle_moves: 4 # Number of random moves to scramble the puzzle + max_moves: 40 # Maximum number of moves allowed per episode +``` +#### Description + +- **`size`**: Determines the dimensions of the puzzle board (`n×n`). +- **`shuffle_moves`**: Controls the initial difficulty by randomly moving tiles to scramble the puzzle. +- **`max_moves`**: Sets an upper limit on the number of actions the agent can take in one episode. + +Grids are generated with sizes ranging from 2 to game_config.size. Each grid starts with a solved state and is shuffled by moving random tiles to the empty space n times, where n is a random number between 1 and `shuffle_moves`. The grid is shuffled using only valid moves. +The `generate_puzzle_datum()` function in [run_grpo_sliding_puzzle.py](../../examples/run_grpo_sliding_puzzle.py) is responsible for generating the dataset. [sliding_puzzle.py](../../nemo_rl/environments/games/sliding_puzzle.py) contains the `SlidingPuzzleGameLogic` class, responsible for puzzle generation and initialization logic. The number of shuffle moves and size of the grid will control puzzle difficulty. + +#### Generation Algorithm +The puzzle configuration is randomly generated by sampling the grid size and number of shuffling moves within the defined maximums: + +```python +def generate_random_config(max_config: dict[str, Any]) -> dict[str, Any]: + """Generate a random config for the sliding puzzle game.""" + shuffle_moves = random.randint(1, max_config.get("shuffle_moves")) + if shuffle_moves % 2 == 0: + shuffle_moves += 1 # Ensure odd number for proper scrambling + return { + "size": random.randint(2, max_config.get("size", 3)), + "shuffle_moves": shuffle_moves, + } + + game_config = generate_random_config(game_config) + initial_game_state = SlidingPuzzleGameLogic.generate(game_config) + initial_render = SlidingPuzzleGameLogic.render(initial_game_state) + welcome_message = SlidingPuzzleGameLogic.init(initial_game_state) + ``` + +### Dataset Size Calculation + +Dataset size is defined by parameters in grpo_sliding_puzzle.yaml: +``` +Training Size = num_prompts_per_step × num_generations_per_prompt × max_num_steps +Validation Size = max_val_samples +``` + +### Data Structure + +Each training sample is returned as a `DatumSpec` dictionary with the following structure: + +```python +datum: DatumSpec = { + "message_log": message_log, # Conversation history + "length": len(tokenized_prompt), # Token count + "extra_env_info": metadata, # Game state metadata + "loss_multiplier": 1.0, # Training weight + "idx": idx, # Sample index + "task_name": task_name, # Task identifier + "stop_strings": [""], # Termination tokens +} +``` + +## Environment Interface + + + +### Core Classes + +The [sliding_puzzle.py](../../nemo_rl/environments/games/sliding_puzzle.py) defines the environment and the logic for interacting with the environment. The core classes used are outlined below: + +#### SlidingPuzzleEnv +The SlidingPuzzleEnv class serves as the main environment, implementing a Ray remote actor for distributed processing and using functions from both the SlidingPuzzleGameLogic and SlidingPuzzleRunner classes to interact with the environment. + +```echanics with stat +@ray.remote +class SlidingPuzzleEnv(EnvironmentInterface): + def __init__(self, cfg: Optional[SlidingPuzzleConfig] = None): + """Initialize environment with configuration.""" + + def step( + self, + message_log_batch: list[LLMMessageLogType], + metadata_batch: list[SlidingPuzzleMetadata], + ) -> EnvironmentReturn: + """Process batch of interactions.""" +``` + +#### SlidingPuzzleGameLogic +The SlidingPuzzleGameLogic class defines the core game mechanics through static methods for puzzle operations and includes functionality for reward calculation. + +```python +class SlidingPuzzleGameLogic: + @staticmethod + def generate(config: dict[str, Any]) -> dict[str, Any]: + """Generate new puzzle with specified configuration.""" + + @staticmethod + def init(game_state: dict[str, Any]) -> str: + """Create welcome message with game rules.""" + + @staticmethod + def step(action: str, game_state: dict[str, Any]) -> tuple[str, float, bool, dict[str, Any]]: + """Execute action and return (response, reward, terminated, new_state).""" + + @staticmethod + def render(game_state: dict[str, Any]) -> str: + """Render current puzzle state as visual grid.""" +``` + +#### SlidingPuzzleRunner + +The SlidingPuzzleRunner class handles turn processing and action management. + +```python +class SlidingPuzzleRunner: + def __init__(self): + """Initialize runner with no persistent state.""" + + def _parse_action(self, text: str) -> Optional[str]: + """Extract action from model response using XML tag parsing.""" + + def process_turn( + self, + message_log: LLMMessageLogType, + metadata: SlidingPuzzleMetadata, + ) -> tuple[dict[str, str], float, bool, Optional[list[str]], Optional[SlidingPuzzleMetadata]]: + """Process single turn and return (response_dict, reward, terminated, stop_strings, updated_metadata).""" +``` + +### Processing Pipeline + +The step function creates a processing pipeline where each class handles specific responsibilities: + +1. **Parse action** (`SlidingPuzzleRunner`): Extracts the action from the model response using XML tag parsing via the `process_turn` method. +2. **Validate Move** (`SlidingPuzzleGameLogic`): Checks if the action is valid for the current game state and then executes the move. +3. **Execute Action** (`SlidingPuzzleGameLogic`): Applies the move to the game state using the `SlidingPuzzleGameLogic.step` method. +4. **Calculate Reward** (`SlidingPuzzleGameLogic`): Assigns a reward based on progress toward solving the puzzle (step function). +5. **Return Results** (`SlidingPuzzleEnv`): Returns the updated interaction state as an `EnvironmentReturn` object. + +## Reward System + +### Reward Structure + +The environment uses a sparse reward scheme designed to encourage complete solution strategies, rather than incremental progress or reward hacking. + +| Condition | Reward | Termination | +|-----------|--------|-------------| +| Valid move (non-solving) | 0.0 | False | +| Invalid move | 0.0 | False | +| Puzzle solved | 1.0 | True | +| Max moves reached | 0.0 | True | +| Invalid action format | 0.0 | False | + +>Goal: The agent receives a reward only upon successfully solving the puzzle, promoting long-horizon planning. + +### Reward Calculation Logic + +```python +def step(action: str, game_state: dict[str, Any]) -> tuple[str, float, bool, dict[str, Any]]: + """Process action and calculate reward.""" + reward = 0.0 + is_terminated = False + + if move_made: + # Check if puzzle is solved + if new_state["grid"] == new_state["solution"]: + reward = 1.0 + is_terminated = True + else: + reward = 0.0 # No reward for non-solving moves + + return response, reward, is_terminated, new_state +``` +## Results + +We fine-tuned [`Qwen/Qwen2.5-1.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) on synthetic data for 120 steps using the following configuration settings: + +``` +game_config: + size: 5 # Size of the puzzle (e.g., 2 for 2x2, 3 for 3x3) + shuffle_moves: 10 # Number of random moves to shuffle the solved state +max_moves: 30 +``` + +The figure below displays training rewards vs. steps, along with validation accuracy. + +![Training Curve](../assets/train-reward-sliding-puzzle.png) + + +![Validation Accuracy](../assets/valid_acc-sliding-puzzle.png) \ No newline at end of file diff --git a/examples/configs/grpo_sliding_puzzle.yaml b/examples/configs/grpo_sliding_puzzle.yaml index 2d72dd1055..fbbcdeecb3 100644 --- a/examples/configs/grpo_sliding_puzzle.yaml +++ b/examples/configs/grpo_sliding_puzzle.yaml @@ -19,7 +19,7 @@ checkpointing: policy: model_name: "Qwen/Qwen2.5-1.5B-Instruct" - max_total_sequence_length: 3072 + max_total_sequence_length: 1024 dtensor_cfg: enabled: true @@ -54,8 +54,8 @@ env: cfg: game_config: size: 5 # Size of the puzzle (e.g., 2 for 2x2, 3 for 3x3) - shuffle_moves: 15 # Number of random moves to shuffle the solved state - max_moves: 50 # Maximum moves allowed per episode + shuffle_moves: 10 # Number of random moves to shuffle the solved state + max_moves: 30 # Maximum moves allowed per episode logger: log_dir: "logs" # Base directory for all logs @@ -73,4 +73,4 @@ logger: run_name: "grpo-dev-sliding_puzzle" 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) + flush_interval: 10 # How often to flush GPU usage metrics to the loggers (in seconds) \ No newline at end of file From 9fb0875d12d1cf6f4b9141f7f1e11c87be99c9c1 Mon Sep 17 00:00:00 2001 From: slikhite-1 Date: Wed, 3 Sep 2025 21:40:26 -0700 Subject: [PATCH 2/3] Update grpo-sliding-puzzle.md Signed-off-by: slikhite-1 --- docs/guides/grpo-sliding-puzzle.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/grpo-sliding-puzzle.md b/docs/guides/grpo-sliding-puzzle.md index 8b69931fe8..d608c0048a 100644 --- a/docs/guides/grpo-sliding-puzzle.md +++ b/docs/guides/grpo-sliding-puzzle.md @@ -175,7 +175,7 @@ The [sliding_puzzle.py](../../nemo_rl/environments/games/sliding_puzzle.py) defi #### SlidingPuzzleEnv The SlidingPuzzleEnv class serves as the main environment, implementing a Ray remote actor for distributed processing and using functions from both the SlidingPuzzleGameLogic and SlidingPuzzleRunner classes to interact with the environment. -```echanics with stat +```python @ray.remote class SlidingPuzzleEnv(EnvironmentInterface): def __init__(self, cfg: Optional[SlidingPuzzleConfig] = None): @@ -291,4 +291,4 @@ The figure below displays training rewards vs. steps, along with validation accu ![Training Curve](../assets/train-reward-sliding-puzzle.png) -![Validation Accuracy](../assets/valid_acc-sliding-puzzle.png) \ No newline at end of file +![Validation Accuracy](../assets/valid_acc-sliding-puzzle.png) From d212d05fe9dd72464797d40117fe08fa5436c809 Mon Sep 17 00:00:00 2001 From: slikhite-1 Date: Fri, 5 Sep 2025 21:44:17 -0700 Subject: [PATCH 3/3] CI issues fixed Signed-off-by: slikhite-1 --- docs/guides/grpo-sliding-puzzle.md | 8 ++++---- docs/index.md | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/guides/grpo-sliding-puzzle.md b/docs/guides/grpo-sliding-puzzle.md index d608c0048a..35833aad34 100644 --- a/docs/guides/grpo-sliding-puzzle.md +++ b/docs/guides/grpo-sliding-puzzle.md @@ -7,11 +7,11 @@ The sliding puzzle task serves as a simple, yet effective example, to illustrate ## Quick Start Guide -#### 1. Install and Set Up NeMo RL with Megatron Backend (Optional) +### 1. Install and Set Up NeMo RL with Megatron Backend (Optional) To get started, clone and set up the NeMo RL repository by initializing submodules, installing CUDA dependencies, and configuring the environment with uv. Refer to [Prerequisites](https://github.com/NVIDIA-NeMo/RL/tree/main?tab=readme-ov-file#prerequisites) for detailed instructions on installation. -#### 2. Train a Model +### 2. Train a Model Train a model to solve the sliding puzzle using GRPO with the default 2×2 configuration. @@ -19,7 +19,7 @@ Train a model to solve the sliding puzzle using GRPO with the default 2×2 confi uv run python examples/run_grpo_sliding_puzzle.py ``` -#### 3. Customize Puzzle Configuration +### 3. Customize Puzzle Configuration By default, this training script uses the configuration in [grpo_sliding_puzzle.yaml](../../examples/configs/grpo_sliding_puzzle.yaml). You can customize parameters with command-line overrides to experiment with different puzzle sizes or levels of difficulty. ```bash @@ -29,7 +29,7 @@ uv run python examples/run_grpo_sliding_puzzle.py \ env.sliding_puzzle_game.cfg.game_config.shuffle_moves=10 ``` -#### 4. Monitor Progress +### 4. Monitor Progress You can enable logging via Weights & Biases and TensorBoard to monitor training metrics such as rewards, success rate, and loss curves. diff --git a/docs/index.md b/docs/index.md index 75c85cbfde..ab6d414dc3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ guides/sft.md guides/dpo.md guides/grpo.md guides/grpo-deepscaler.md +guides/grpo-sliding-puzzle.md guides/rm.md guides/eval.md guides/deepseek.md