shahidul034 commited on
Commit
ad5ba74
·
verified ·
1 Parent(s): 1267481

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. code/RL_model/verl/verl_train/verl/experimental/agent_loop/__init__.py +21 -0
  2. code/RL_model/verl/verl_train/verl/experimental/agent_loop/agent_loop.py +1022 -0
  3. code/RL_model/verl/verl_train/verl/experimental/agent_loop/prometheus_utils.py +110 -0
  4. code/RL_model/verl/verl_train/verl/experimental/agent_loop/single_turn_agent_loop.py +84 -0
  5. code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_agent_loop.py +475 -0
  6. code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_parser.py +161 -0
  7. code/RL_model/verl/verl_train/verl/experimental/agent_loop/utils.py +108 -0
  8. code/RL_model/verl/verl_train/verl/experimental/dataset/__init__.py +13 -0
  9. code/RL_model/verl/verl_train/verl/experimental/dataset/sampler.py +40 -0
  10. code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/__init__.py +13 -0
  11. code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/dynamicgen_dataset.py +112 -0
  12. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README.md +599 -0
  13. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README_zh.md +517 -0
  14. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/__init__.py +20 -0
  15. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/agent_loop.py +370 -0
  16. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_single_turn_agent_loop.py +115 -0
  17. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_tool_agent_loop.py +281 -0
  18. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/base_detach_sync.py +238 -0
  19. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/checkpoint_engine.py +522 -0
  20. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_megatron_trainer.yaml +76 -0
  21. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_trainer.yaml +76 -0
  22. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/detach_utils.py +363 -0
  23. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp2_utils.py +125 -0
  24. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp_workers.py +247 -0
  25. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_main.py +312 -0
  26. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_rollouter.py +793 -0
  27. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_trainer.py +612 -0
  28. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_utils.py +99 -0
  29. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_worker.py +267 -0
  30. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/message_queue.py +265 -0
  31. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/param_sync.py +173 -0
  32. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/ray_trainer.py +538 -0
  33. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/__init__.py +13 -0
  34. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/sglang_async_server.py +189 -0
  35. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_30b_a3b_base_math_fsdp.sh +191 -0
  36. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_async_retool.sh +141 -0
  37. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_16_16.sh +162 -0
  38. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_32_32.sh +162 -0
  39. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_12.sh +164 -0
  40. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_4.sh +164 -0
  41. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64.sh +162 -0
  42. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64_mis.sh +173 -0
  43. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_8_8.sh +162 -0
  44. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/geo3k_qwen25vl_7b_megatron_4_4.sh +111 -0
  45. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32.sh +230 -0
  46. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32_mis.sh +239 -0
  47. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/runtime_env.yaml +4 -0
  48. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/unittest/simple_streaming_demo.py +176 -0
  49. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/vllm_rollout/__init__.py +13 -0
  50. code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/vllm_rollout/vllm_async_server.py +148 -0
code/RL_model/verl/verl_train/verl/experimental/agent_loop/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .agent_loop import AgentLoopBase, AgentLoopManager, AgentLoopWorker, AsyncLLMServerManager
16
+ from .single_turn_agent_loop import SingleTurnAgentLoop
17
+ from .tool_agent_loop import ToolAgentLoop
18
+
19
+ _ = [SingleTurnAgentLoop, ToolAgentLoop]
20
+
21
+ __all__ = ["AgentLoopBase", "AgentLoopManager", "AsyncLLMServerManager", "AgentLoopWorker"]
code/RL_model/verl/verl_train/verl/experimental/agent_loop/agent_loop.py ADDED
@@ -0,0 +1,1022 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import heapq
16
+ import logging
17
+ import os
18
+ import random
19
+ from abc import ABC, abstractmethod
20
+ from typing import Any, Optional
21
+ from uuid import uuid4
22
+
23
+ import hydra
24
+ import numpy as np
25
+ import ray
26
+ import torch
27
+ from cachetools import LRUCache
28
+ from omegaconf import DictConfig, OmegaConf
29
+ from PIL import Image
30
+ from pydantic import BaseModel, ConfigDict
31
+ from tensordict import TensorDict
32
+ from transformers import AutoProcessor, AutoTokenizer
33
+
34
+ from verl.experimental.agent_loop.prometheus_utils import update_prometheus_config
35
+ from verl.experimental.agent_loop.utils import resolve_config_path
36
+ from verl.experimental.reward_loop import RewardLoopWorker
37
+ from verl.protocol import DataProto
38
+ from verl.single_controller.ray.base import RayResourcePool, RayWorkerGroup
39
+ from verl.utils import hf_processor, hf_tokenizer
40
+ from verl.utils.chat_template import initialize_system_prompt
41
+ from verl.utils.dataset.rl_dataset import RLHFDataset, get_dataset_class
42
+ from verl.utils.fs import copy_to_local
43
+ from verl.utils.model import compute_position_id_with_mask
44
+ from verl.utils.ray_utils import get_event_loop
45
+ from verl.utils.rollout_trace import (
46
+ RolloutTraceConfig,
47
+ rollout_trace_attr,
48
+ rollout_trace_op,
49
+ )
50
+ from verl.utils.transferqueue_utils import tqbridge
51
+ from verl.workers.rollout.replica import TokenOutput, get_rollout_replica_class
52
+
53
+ logger = logging.getLogger(__file__)
54
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
55
+
56
+
57
+ class AsyncLLMServerManager:
58
+ """
59
+ A class to manage multiple OpenAI compatible LLM servers. This class provides
60
+ - Load balance: least requests load balancing
61
+ - Sticky session: send multi-turn chat completions to same server for automatic prefix caching
62
+ """
63
+
64
+ def __init__(self, config: DictConfig, server_handles: list[ray.actor.ActorHandle], max_cache_size: int = 10000):
65
+ """Initialize the AsyncLLMServerManager.
66
+
67
+ Args:
68
+ config (DictConfig): YAML config.
69
+ server_handles (List[ray.actor.ActorHandle]): OpenAI compatible LLM server actor handles.
70
+ max_cache_size (int, optional): max cache size for request_id to server mapping. Defaults to 10000.
71
+ """
72
+ self.config = config
73
+ self.server_handles = server_handles
74
+ random.shuffle(self.server_handles)
75
+
76
+ # Least requests load balancing
77
+ self.weighted_serveres = [[0, idx, server] for idx, server in enumerate(self.server_handles)]
78
+ heapq.heapify(self.weighted_serveres)
79
+
80
+ # LRU cache to map request_id to server
81
+ self.request_id_to_server = LRUCache(maxsize=max_cache_size)
82
+
83
+ def _choose_server(self, request_id: str) -> ray.actor.ActorHandle:
84
+ # TODO: implement server pressure awareness load balancing
85
+ if request_id in self.request_id_to_server:
86
+ return self.request_id_to_server[request_id]
87
+
88
+ _, _, server = self.weighted_serveres[0]
89
+ self.weighted_serveres[0][0] += 1
90
+ heapq.heapreplace(self.weighted_serveres, self.weighted_serveres[0])
91
+ self.request_id_to_server[request_id] = server
92
+ return server
93
+
94
+ @rollout_trace_op
95
+ async def generate(
96
+ self,
97
+ request_id,
98
+ *,
99
+ prompt_ids: list[int],
100
+ sampling_params: dict[str, Any],
101
+ image_data: Optional[list[Any]] = None,
102
+ video_data: Optional[list[Any]] = None,
103
+ ) -> TokenOutput:
104
+ """Generate tokens from prompt ids.
105
+
106
+ Args:
107
+ request_id (str): request id for sticky session.
108
+ prompt_ids (List[int]): List of prompt token ids.
109
+ sampling_params (Dict[str, Any]): Sampling parameters for the chat completion.
110
+
111
+ Returns:
112
+ TokenOutput: token output
113
+ """
114
+ server = self._choose_server(request_id)
115
+ output = await server.generate.remote(
116
+ request_id=uuid4().hex, # use new request_id for each turn
117
+ prompt_ids=prompt_ids,
118
+ sampling_params=sampling_params,
119
+ image_data=image_data,
120
+ video_data=video_data,
121
+ )
122
+ return output
123
+
124
+
125
+ class AgentLoopMetrics(BaseModel):
126
+ """Agent loop performance metrics."""
127
+
128
+ generate_sequences: float = 0.0
129
+ tool_calls: float = 0.0
130
+ num_preempted: int = -1 # -1 means not available
131
+
132
+
133
+ class AgentLoopOutput(BaseModel):
134
+ """Agent loop output."""
135
+
136
+ prompt_ids: list[int]
137
+ """Prompt token ids."""
138
+ response_ids: list[int]
139
+ """Response token ids including LLM generated token, tool response token."""
140
+ response_mask: list[int]
141
+ """Response mask, 1 for LLM generated token, 0 for tool response token."""
142
+ response_logprobs: Optional[list[float]] = None
143
+ """Log probabilities for the response tokens."""
144
+ routed_experts: Optional[Any] = None
145
+ """Routed experts for the total tokens."""
146
+ multi_modal_data: Optional[dict[str, Any]] = None
147
+ """Multi-modal data for multi-modal tools."""
148
+ reward_score: Optional[float] = None
149
+ """Reward score for the trajectory."""
150
+ num_turns: int = 0
151
+ """Number of chat turns, including user, assistant, tool."""
152
+ metrics: AgentLoopMetrics
153
+ """Auxiliary performance metrics"""
154
+ extra_fields: dict[str, Any] = {}
155
+ """Extra fields for dynamic addition."""
156
+
157
+
158
+ class _InternalAgentLoopOutput(AgentLoopOutput):
159
+ """Internal agent loop output with padded sequences."""
160
+
161
+ model_config = ConfigDict(arbitrary_types_allowed=True)
162
+
163
+ prompt_ids: torch.Tensor
164
+ """Padded prompt token ids."""
165
+ response_ids: torch.Tensor
166
+ """Padded response token ids."""
167
+ input_ids: torch.Tensor
168
+ """Padded input ids(prompt_ids + response_ids)."""
169
+ position_ids: torch.Tensor
170
+ """Padded position ids."""
171
+ response_mask: torch.Tensor
172
+ """Padded response mask."""
173
+ attention_mask: torch.Tensor
174
+ """Padded attention mask."""
175
+ response_logprobs: Optional[torch.Tensor] = None
176
+ """Padded log probabilities for the response tokens."""
177
+ routed_experts: Optional[torch.Tensor] = None
178
+ """Padded routed experts for the total tokens."""
179
+ multi_modal_inputs: Optional[dict[str, torch.Tensor]] = None
180
+ """Multi-modal inputs for processors (e.g., pixel_values, image_grid_thw)."""
181
+ extra_fields: dict[str, Any] = {}
182
+ """Extra fields for dynamic addition."""
183
+
184
+
185
+ class DictConfigWrap:
186
+ """Wrapper for DictConfig to avoid hydra.utils.instantiate recursive resolve."""
187
+
188
+ def __init__(self, config: DictConfig):
189
+ self.config = config
190
+
191
+
192
+ class AgentLoopBase(ABC):
193
+ """An agent loop takes an input message, chat with OpenAI compatible LLM server and interact with various
194
+ environments."""
195
+
196
+ def __init__(
197
+ self,
198
+ trainer_config: DictConfigWrap,
199
+ server_manager: AsyncLLMServerManager,
200
+ tokenizer: AutoTokenizer,
201
+ processor: AutoProcessor,
202
+ dataset_cls: type[RLHFDataset],
203
+ dataset_config: DictConfigWrap,
204
+ **kwargs,
205
+ ):
206
+ """Initialize agent loop, each sample will have its own loop instance.
207
+
208
+ Args:
209
+ trainer_config (DictConfigWrap): trainer config.
210
+ server_manager (AsyncLLMServerManager): OpenAI compatible LLM server manager.
211
+ tokenizer (AutoTokenizer): Tokenizer for tokenize messages.
212
+ processor (AutoProcessor): Processor for process messages.
213
+ dataset_cls (type[Dataset]): Dataset class for creating dataset, Defaults to RLHFDataset.
214
+ dataset_config (DictConfigWrap): Dataset config.
215
+ """
216
+ self.config = trainer_config.config
217
+ self.server_manager = server_manager
218
+ self.tokenizer = tokenizer
219
+ self.processor = processor
220
+ self.dataset_cls = dataset_cls
221
+ self.dataset_config = dataset_config.config
222
+ self.apply_chat_template_kwargs = self.dataset_config.get("apply_chat_template_kwargs", {})
223
+ self.system_prompt = initialize_system_prompt(self.tokenizer, **self.apply_chat_template_kwargs)
224
+ self.loop = get_event_loop()
225
+
226
+ async def process_vision_info(self, messages: list[dict]) -> dict:
227
+ """Extract images and videos from messages.
228
+
229
+ Args:
230
+ messages (list[dict]): Input messages.
231
+
232
+ Returns:
233
+ dict: Multi-modal data with keys "images" and "videos".
234
+ """
235
+ multi_modal_data = {}
236
+ if self.processor is not None:
237
+ images, videos = await self.dataset_cls.process_vision_info(
238
+ messages, image_patch_size=self.processor.image_processor.patch_size, config=self.dataset_config
239
+ )
240
+ if images is not None:
241
+ multi_modal_data["images"] = images
242
+ if videos is not None:
243
+ multi_modal_data["videos"] = videos
244
+
245
+ return multi_modal_data
246
+
247
+ async def apply_chat_template(
248
+ self,
249
+ messages: list[dict],
250
+ tools: list[dict] = None,
251
+ images: list[Image.Image] = None,
252
+ videos: list[tuple[torch.Tensor, dict]] = None,
253
+ remove_system_prompt: bool = False,
254
+ ):
255
+ """Apply chat template to messages with optional tools, images, and videos.
256
+
257
+ Args:
258
+ messages (list[dict]): Input messages.
259
+ tools (list[dict], optional): Tools schemas. Defaults to None.
260
+ images (list[Image.Image], optional): Input images. Defaults to None.
261
+ videos (list[tuple[torch.Tensor, dict]], optional): Input videos. Defaults to None.
262
+ remove_system_prompt (bool, optional): Whether to remove system prompt. Defaults to False.
263
+
264
+ Returns:
265
+ list[int]: Prompt token ids.
266
+ """
267
+ if self.processor is not None:
268
+ raw_prompt = await self.loop.run_in_executor(
269
+ None,
270
+ lambda: self.processor.apply_chat_template(
271
+ messages,
272
+ tools=tools,
273
+ add_generation_prompt=True,
274
+ tokenize=False,
275
+ **self.apply_chat_template_kwargs,
276
+ ),
277
+ )
278
+
279
+ # split the videos and according metadatas
280
+ if videos is not None:
281
+ videos, video_metadatas = zip(*videos, strict=False)
282
+ videos, video_metadatas = list(videos), list(video_metadatas)
283
+ else:
284
+ video_metadatas = None
285
+
286
+ model_inputs = self.processor(
287
+ text=[raw_prompt],
288
+ images=images,
289
+ videos=videos,
290
+ video_metadatas=video_metadatas,
291
+ return_tensors="pt",
292
+ do_sample_frames=False,
293
+ )
294
+ prompt_ids = model_inputs.pop("input_ids").squeeze(0).tolist()
295
+ else:
296
+ prompt_ids = await self.loop.run_in_executor(
297
+ None,
298
+ lambda: self.tokenizer.apply_chat_template(
299
+ messages,
300
+ tools=tools,
301
+ add_generation_prompt=True,
302
+ tokenize=True,
303
+ **self.apply_chat_template_kwargs,
304
+ ),
305
+ )
306
+
307
+ if remove_system_prompt:
308
+ prompt_ids = prompt_ids[len(self.system_prompt) :]
309
+
310
+ return prompt_ids
311
+
312
+ @abstractmethod
313
+ async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
314
+ """Run agent loop to interact with LLM server and environment.
315
+
316
+ Args:
317
+ sampling_params (Dict[str, Any]): LLM sampling params.
318
+ **kwargs: dataset fields from `verl.utils.dataset.RLHFDataset`.
319
+
320
+ Returns:
321
+ AgentLoopOutput: Agent loop output.
322
+ """
323
+ raise NotImplementedError
324
+
325
+
326
+ """Agent loop registry: key is agent_name, value is a dict of agent loop config
327
+ used by hydra.utils.instantiate to initialize agent loop instance.
328
+
329
+ https://hydra.cc/docs/advanced/instantiate_objects/overview/
330
+ """
331
+ _agent_loop_registry: dict[str, dict] = {}
332
+
333
+
334
+ def register(agent_name: str):
335
+ """Register agent loop class."""
336
+
337
+ def decorator(subclass: type[AgentLoopBase]) -> type[AgentLoopBase]:
338
+ fqdn = f"{subclass.__module__}.{subclass.__qualname__}"
339
+ _agent_loop_registry[agent_name] = {"_target_": fqdn}
340
+ return subclass
341
+
342
+ return decorator
343
+
344
+
345
+ class AgentLoopWorker:
346
+ """Agent loop worker takes a batch of messages and run each message in an agent loop."""
347
+
348
+ def __init__(
349
+ self,
350
+ config: DictConfig,
351
+ server_handles: list[ray.actor.ActorHandle],
352
+ reward_router_address: str = None,
353
+ ):
354
+ """Initialize agent loop manager.
355
+ Args:
356
+ config (DictConfig): YAML config.
357
+ server_handles (List[ray.actor.ActorHandle]): OpenAI compatible LLM server actor handles.
358
+ reward_router_address (str): reward router address.
359
+ """
360
+ self.config = config
361
+
362
+ # for recipe to change
363
+ if not hasattr(self, "server_manager"):
364
+ self.server_manager = AsyncLLMServerManager(config, server_handles)
365
+
366
+ self.dataset_cls = get_dataset_class(config.data)
367
+ self.reward_router_address = reward_router_address
368
+
369
+ model_path = config.actor_rollout_ref.model.path
370
+ self.model_name = "/".join(model_path.split("/")[-2:])
371
+ local_path = copy_to_local(config.actor_rollout_ref.model.path)
372
+ self.tokenizer = hf_tokenizer(local_path, trust_remote_code=True)
373
+ self.processor = hf_processor(local_path, trust_remote_code=True)
374
+
375
+ agent_loop_config_path = config.actor_rollout_ref.rollout.agent.agent_loop_config_path
376
+ if agent_loop_config_path:
377
+ resolved_path = resolve_config_path(agent_loop_config_path)
378
+ agent_loop_configs = OmegaConf.load(resolved_path)
379
+ for agent_loop_config in agent_loop_configs:
380
+ _agent_loop_registry[agent_loop_config.name] = agent_loop_config
381
+ if self.config.actor_rollout_ref.model.get("custom_chat_template", None) is not None:
382
+ if self.processor is not None:
383
+ self.processor.chat_template = self.config.actor_rollout_ref.model.custom_chat_template
384
+ self.tokenizer.chat_template = self.config.actor_rollout_ref.model.custom_chat_template
385
+
386
+ use_reward_loop = True if self.config.reward_model.use_reward_loop else None
387
+ self.use_reward_loop = use_reward_loop
388
+ if use_reward_loop and not hasattr(self, "reward_loop_worker"):
389
+ self.reward_loop_worker = RewardLoopWorker.options(
390
+ scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
391
+ node_id=ray.get_runtime_context().get_node_id(),
392
+ soft=False,
393
+ ),
394
+ ).remote(self.config, self.reward_router_address)
395
+
396
+ trace_config = self.config.actor_rollout_ref.rollout.get("trace", {})
397
+ RolloutTraceConfig.init(
398
+ self.config.trainer.project_name,
399
+ self.config.trainer.experiment_name,
400
+ trace_config.get("backend"),
401
+ trace_config.get("token2text", False),
402
+ trace_config.get("max_samples_per_step_per_worker", None),
403
+ )
404
+
405
+ @tqbridge()
406
+ async def generate_sequences(self, batch: DataProto) -> DataProto:
407
+ """Generate sequences from agent loop.
408
+
409
+ Args:
410
+ batch (DataProto): Input batch.
411
+
412
+ Returns:
413
+ DataProto: Output batch.
414
+ - prompts: [bsz, prompt_length], prompt token ids from dataset.
415
+ - responses: [bsz, response_length], output token ids include response tokens
416
+ from LLM generation and observation tokens from tool_calls.
417
+ - response_mask: [bsz, response_length], 1 for LLM generated tokens, 0 for observation/padding tokens.
418
+ - input_ids: [bsz, prompt_length + response_length], whole sequence token ids, including prompt tokens
419
+ and response tokens.
420
+ - attention_mask: [bsz, prompt_length + response_length], 0 for padding tokens, 1 for other tokens.
421
+ - position_ids: [bsz, prompt_length + response_length], incremental position ids.
422
+
423
+ For multi-turn conversations:
424
+ responses: |<- LLM generation ->|<- tool_calls ->|<- LLM generation ->|<- padding ->|
425
+ response_mask: | 1, 1, 1, ..., 1, 1 | 0, 0, .., 0, 0 | 1, 1, 1, ..., 1, 1 | 0, 0, ..., 0|
426
+ """
427
+ config = self.config.actor_rollout_ref.rollout
428
+ sampling_params = dict(
429
+ temperature=config.temperature,
430
+ top_p=config.top_p,
431
+ top_k=config.top_k,
432
+ repetition_penalty=1.0,
433
+ logprobs=config.calculate_log_probs,
434
+ )
435
+
436
+ # override sampling params for validation
437
+ if batch.meta_info.get("validate", False):
438
+ sampling_params["top_p"] = config.val_kwargs.top_p
439
+ sampling_params["top_k"] = config.val_kwargs.top_k
440
+ sampling_params["temperature"] = config.val_kwargs.temperature
441
+
442
+ # by default, we assume it's a single turn agent
443
+ if "agent_name" not in batch.non_tensor_batch:
444
+ default_agent_loop = config.agent.default_agent_loop
445
+ batch.non_tensor_batch["agent_name"] = np.array([default_agent_loop] * len(batch), dtype=object)
446
+
447
+ if "index" in batch.non_tensor_batch:
448
+ index = batch.non_tensor_batch["index"]
449
+ else:
450
+ index = np.arange(len(batch))
451
+
452
+ max_samples_per_worker = RolloutTraceConfig.get_instance().max_samples_per_step_per_worker
453
+
454
+ # For n rollouts per sample, we trace all n rollouts for selected samples
455
+ # Note: This sampling happens per-worker, so total traces = max_samples_per_worker * num_workers * n
456
+ if max_samples_per_worker is not None:
457
+ unique_sample_indices = np.unique(index)
458
+ if max_samples_per_worker < len(unique_sample_indices):
459
+ selected_samples = set(
460
+ np.random.choice(unique_sample_indices, max_samples_per_worker, replace=False).tolist()
461
+ )
462
+ traced_indices = set(i for i in range(len(batch)) if index[i] in selected_samples)
463
+ else:
464
+ traced_indices = set(range(len(batch)))
465
+ else:
466
+ traced_indices = set(range(len(batch)))
467
+
468
+ trajectory_info = await get_trajectory_info(
469
+ batch.meta_info.get("global_steps", -1), index.tolist(), batch.meta_info.get("validate", False)
470
+ )
471
+
472
+ tasks = []
473
+ for i in range(len(batch)):
474
+ trace_this_sample = i in traced_indices
475
+ kwargs = {k: v[i] for k, v in batch.non_tensor_batch.items()}
476
+ tasks.append(
477
+ asyncio.create_task(
478
+ self._run_agent_loop(sampling_params, trajectory_info[i], trace=trace_this_sample, **kwargs)
479
+ )
480
+ )
481
+ outputs = await asyncio.gather(*tasks)
482
+
483
+ output = self._postprocess(outputs)
484
+
485
+ return output
486
+
487
+ async def _run_agent_loop(
488
+ self,
489
+ sampling_params: dict[str, Any],
490
+ trajectory: dict[str, Any],
491
+ *,
492
+ agent_name: str,
493
+ trace: bool = True,
494
+ **kwargs,
495
+ ) -> _InternalAgentLoopOutput:
496
+ with rollout_trace_attr(
497
+ step=trajectory["step"],
498
+ sample_index=trajectory["sample_index"],
499
+ rollout_n=trajectory["rollout_n"],
500
+ validate=trajectory["validate"],
501
+ name="agent_loop",
502
+ trace=trace,
503
+ ):
504
+ assert agent_name in _agent_loop_registry, (
505
+ f"Agent loop {agent_name} not registered, registered agent loops: {_agent_loop_registry.keys()}"
506
+ )
507
+
508
+ agent_loop_config = _agent_loop_registry[agent_name]
509
+ agent_loop = hydra.utils.instantiate(
510
+ config=agent_loop_config,
511
+ trainer_config=DictConfigWrap(config=self.config),
512
+ server_manager=self.server_manager,
513
+ tokenizer=self.tokenizer,
514
+ processor=self.processor,
515
+ dataset_cls=self.dataset_cls,
516
+ dataset_config=DictConfigWrap(self.config.data),
517
+ )
518
+ output: AgentLoopOutput = await agent_loop.run(sampling_params, **kwargs)
519
+ return await self._agent_loop_postprocess(output, **kwargs)
520
+
521
+ async def _agent_loop_postprocess(self, output, **kwargs) -> _InternalAgentLoopOutput:
522
+ """Perform post-processing operations on the output of each individual agent loop."""
523
+ output.extra_fields["raw_prompt"] = kwargs["raw_prompt"]
524
+
525
+ # Some AgentLoop may have already computed the reward score, e.g SWE-agent.
526
+
527
+ # NOTE: consistent with the legacy batch version of generate_sequences that existed in the
528
+ # deprecated vLLM SPMD rollout implementation.
529
+ # prompt_ids: left padded with zeros (e.g., [0,0,0,0,1,2,3,4])
530
+ # response_ids: right padded with zeros (e.g., [5,6,7,8,0,0,0,0])
531
+ # input_ids: concatenation of prompt + response
532
+ # Mask:
533
+ # For example, if the prompt is [1,2,3,4] and the response is [5,6,7,(tool start)8,9(tool end),10,11,12]
534
+ # - prompt_attention_mask: 0s for padding, 1s for tokens
535
+ # e.g., [0,0,0,0,1,1,1,1]
536
+ # - response_attention_mask: 0s for padding, 1s for tokens
537
+ # e.g., [1,1,1,1,1,1,1,1,1,1,1,0,0,0,0]
538
+ # attention_mask: concatenation of prompt_attention_mask and response_attention_mask
539
+ # e.g., [0,0,0,0,1,1,1,1(prompt),1,1,1,1,1,1,1,1,1,1,1,0,0,0,0(response)]
540
+ # - response_mask: 1s for LLM generated tokens, 0 for tool response/padding tokens
541
+ # e.g., [1,1,1,1,1,1,1,(tool start),0,0(tool end),1,1,0,0,0,0]
542
+ # - position_ids: sequential positions for tokens, starting at 0
543
+ # e.g., [0,0,0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,0,0,0,0]
544
+
545
+ # TODO(wuxibin): remove padding and use tensordict.
546
+ self.tokenizer.padding_side = "left"
547
+ prompt_output = self.tokenizer.pad(
548
+ {"input_ids": output.prompt_ids},
549
+ padding="max_length",
550
+ max_length=self.config.actor_rollout_ref.rollout.prompt_length,
551
+ return_tensors="pt",
552
+ return_attention_mask=True,
553
+ )
554
+ if prompt_output["input_ids"].dim() == 1:
555
+ prompt_output["input_ids"] = prompt_output["input_ids"].unsqueeze(0)
556
+ prompt_output["attention_mask"] = prompt_output["attention_mask"].unsqueeze(0)
557
+
558
+ self.tokenizer.padding_side = "right"
559
+ response_output = self.tokenizer.pad(
560
+ {"input_ids": output.response_ids},
561
+ padding="max_length",
562
+ max_length=self.config.actor_rollout_ref.rollout.response_length,
563
+ return_tensors="pt",
564
+ return_attention_mask=True,
565
+ )
566
+ if response_output["input_ids"].dim() == 1:
567
+ response_output["input_ids"] = response_output["input_ids"].unsqueeze(0)
568
+ response_output["attention_mask"] = response_output["attention_mask"].unsqueeze(0)
569
+
570
+ response_mask_output = self.tokenizer.pad(
571
+ {"input_ids": output.response_mask},
572
+ padding="max_length",
573
+ max_length=self.config.actor_rollout_ref.rollout.response_length,
574
+ return_tensors="pt",
575
+ return_attention_mask=False,
576
+ )
577
+ if response_mask_output["input_ids"].dim() == 1:
578
+ response_mask_output["input_ids"] = response_mask_output["input_ids"].unsqueeze(0)
579
+
580
+ response_logprobs = None
581
+ if output.response_logprobs is not None:
582
+ pad_size = self.config.actor_rollout_ref.rollout.response_length - len(output.response_logprobs)
583
+ response_logprobs = torch.tensor(output.response_logprobs + [0.0] * pad_size).unsqueeze(0)
584
+
585
+ response_mask = response_mask_output["input_ids"] * response_output["attention_mask"]
586
+ attention_mask = torch.cat([prompt_output["attention_mask"], response_output["attention_mask"]], dim=1)
587
+ input_ids = torch.cat([prompt_output["input_ids"], response_output["input_ids"]], dim=1)
588
+
589
+ routed_experts = None
590
+ if output.routed_experts is not None:
591
+ total_length = input_ids.shape[1]
592
+ length, layer_num, topk_num = output.routed_experts.shape
593
+ if isinstance(output.routed_experts, np.ndarray):
594
+ experts_tensor = torch.from_numpy(output.routed_experts)
595
+ elif isinstance(output.routed_experts, torch.Tensor):
596
+ experts_tensor = output.routed_experts
597
+ else:
598
+ raise TypeError(f"Unsupported type for routed_experts: {type(output.routed_experts)}")
599
+ routed_experts = torch.zeros(1, total_length, layer_num, topk_num, dtype=experts_tensor.dtype)
600
+
601
+ # Calculate start position: left padding means original prompt starts at the end
602
+ start_pos = prompt_output["input_ids"].shape[1] - len(output.prompt_ids)
603
+ end_pos = min(start_pos + length, total_length)
604
+
605
+ # Add boundary checks for robustness
606
+ if start_pos < 0 or end_pos > total_length:
607
+ raise ValueError(
608
+ f"Invalid position range: start_pos={start_pos}, end_pos={end_pos}, total_length={total_length}"
609
+ )
610
+
611
+ routed_experts[:, start_pos:end_pos] = experts_tensor.unsqueeze(0)
612
+
613
+ multi_modal_inputs = self._compute_multi_modal_inputs(output, input_ids)
614
+ position_ids = self._compute_position_ids(input_ids, attention_mask, multi_modal_inputs)
615
+ await self._compute_score(
616
+ output,
617
+ prompts=prompt_output["input_ids"],
618
+ responses=response_output["input_ids"],
619
+ attention_mask=attention_mask,
620
+ input_ids=input_ids,
621
+ position_ids=position_ids,
622
+ kwargs=kwargs,
623
+ )
624
+
625
+ return _InternalAgentLoopOutput(
626
+ prompt_ids=prompt_output["input_ids"],
627
+ response_ids=response_output["input_ids"],
628
+ input_ids=input_ids,
629
+ position_ids=position_ids,
630
+ response_mask=response_mask,
631
+ attention_mask=attention_mask,
632
+ response_logprobs=response_logprobs,
633
+ routed_experts=routed_experts,
634
+ multi_modal_inputs=multi_modal_inputs,
635
+ multi_modal_data=output.multi_modal_data,
636
+ reward_score=output.reward_score,
637
+ num_turns=output.num_turns,
638
+ metrics=output.metrics,
639
+ extra_fields=output.extra_fields,
640
+ )
641
+
642
+ def _compute_multi_modal_inputs(self, output, input_ids) -> dict[str, torch.Tensor]:
643
+ """Compute multi-modal inputs with image and video."""
644
+ multi_modal_inputs = {}
645
+ if self.processor is None:
646
+ return multi_modal_inputs
647
+
648
+ images = output.multi_modal_data.get("images")
649
+ videos = output.multi_modal_data.get("videos")
650
+ # split the videos and according metadatas
651
+ if videos is not None:
652
+ videos, video_metadatas = zip(*videos, strict=False)
653
+ videos, video_metadatas = list(videos), list(video_metadatas)
654
+ else:
655
+ video_metadatas = None
656
+ current_text = self.tokenizer.decode(input_ids.squeeze(0), skip_special_tokens=True)
657
+ multi_modal_inputs = self.processor(
658
+ text=[current_text],
659
+ images=images,
660
+ videos=videos,
661
+ video_metadatas=video_metadatas,
662
+ return_tensors="pt",
663
+ do_sample_frames=False,
664
+ )
665
+ multi_modal_inputs.pop("input_ids", None)
666
+ multi_modal_inputs.pop("attention_mask", None)
667
+
668
+ # We must use dict(multi_modal_inputs) to convert BatchFeature values to a new dict
669
+ # because np.array() only keeps the keys for BatchFeature.
670
+ multi_modal_inputs = dict(multi_modal_inputs.convert_to_tensors("pt"))
671
+ image_grid_thw = multi_modal_inputs.get("image_grid_thw")
672
+ if image_grid_thw is not None:
673
+ images_seqlens = torch.repeat_interleave(image_grid_thw[:, 1] * image_grid_thw[:, 2], image_grid_thw[:, 0])
674
+ multi_modal_inputs["images_seqlens"] = images_seqlens
675
+ return multi_modal_inputs
676
+
677
+ def _compute_position_ids(self, input_ids, attention_mask, multi_modal_inputs) -> torch.Tensor:
678
+ """Compute position ids for multi-modal inputs."""
679
+ if self.processor is None:
680
+ return compute_position_id_with_mask(attention_mask) # (1, seq_len)
681
+
682
+ image_grid_thw = multi_modal_inputs.get("image_grid_thw")
683
+ video_grid_thw = multi_modal_inputs.get("video_grid_thw")
684
+
685
+ # Model's get_rope_index has been dynamically bind to the processor.
686
+ vision_position_ids, _ = self.processor.get_rope_index(
687
+ input_ids=input_ids,
688
+ image_grid_thw=image_grid_thw,
689
+ video_grid_thw=video_grid_thw,
690
+ attention_mask=attention_mask,
691
+ )
692
+ vision_position_ids = vision_position_ids.transpose(0, 1) # (3, 1, seq_len) => (1, 3, seq_len)
693
+
694
+ valid_mask = attention_mask[0].bool()
695
+ text_position_ids = torch.ones((1, len(input_ids[0])), dtype=torch.long)
696
+ text_position_ids[0, valid_mask] = torch.arange(valid_mask.sum().item())
697
+ text_position_ids = text_position_ids.unsqueeze(0)
698
+ position_ids = torch.cat((text_position_ids, vision_position_ids), dim=1) # (1, 4, seq_length)
699
+ return position_ids
700
+
701
+ async def _compute_score(self, output, prompts, responses, attention_mask, input_ids, position_ids, kwargs):
702
+ """Compute reward score for single sample."""
703
+ enable_async_reward = (
704
+ self.reward_router_address is not None and self.config.reward_model.enable_resource_pool
705
+ ) or not self.config.reward_model.enable
706
+
707
+ if output.reward_score is None and enable_async_reward and self.use_reward_loop:
708
+ batch = TensorDict(
709
+ {
710
+ "prompts": prompts, # [1, prompt_length]
711
+ "responses": responses, # [1, response_length]
712
+ "attention_mask": attention_mask, # [1, prompt_length + response_length]
713
+ "input_ids": input_ids, # [1, prompt_length + response_length]
714
+ "position_ids": position_ids,
715
+ },
716
+ batch_size=1,
717
+ )
718
+ non_tensor_batch = {
719
+ **{k: np.array([v]) for k, v in kwargs.items()},
720
+ "__num_turns__": np.array([output.num_turns]),
721
+ "tool_extra_fields": np.array([output.extra_fields], dtype=object),
722
+ }
723
+
724
+ data = DataProto(
725
+ batch=batch,
726
+ non_tensor_batch=non_tensor_batch,
727
+ )
728
+ result = await self.reward_loop_worker.compute_score.remote(data)
729
+ output.reward_score = result["reward_score"]
730
+ output.extra_fields["reward_extra_info"] = result["reward_extra_info"]
731
+
732
+ def _postprocess(self, inputs: list[_InternalAgentLoopOutput]) -> DataProto:
733
+ """Process the padded outputs from _run_agent_loop and combine them into a batch."""
734
+ # Convert lists back to tensors and stack them to create a batch.
735
+ prompt_ids = torch.cat([input.prompt_ids for input in inputs], dim=0)
736
+ response_ids = torch.cat([input.response_ids for input in inputs], dim=0)
737
+ response_mask = torch.cat([input.response_mask for input in inputs], dim=0)
738
+ attention_mask = torch.cat([input.attention_mask for input in inputs], dim=0)
739
+ input_ids = torch.cat([input.input_ids for input in inputs], dim=0)
740
+ position_ids = torch.cat([input.position_ids for input in inputs], dim=0)
741
+ optional_outputs = {}
742
+ if inputs[0].response_logprobs is not None:
743
+ optional_outputs["rollout_log_probs"] = torch.cat([input.response_logprobs for input in inputs], dim=0)
744
+ if inputs[0].routed_experts is not None:
745
+ optional_outputs["routed_experts"] = torch.cat([input.routed_experts for input in inputs], dim=0)
746
+
747
+ batch = TensorDict(
748
+ {
749
+ "prompts": prompt_ids, # [bsz, prompt_length]
750
+ "responses": response_ids, # [bsz, response_length]
751
+ "response_mask": response_mask, # [bsz, response_length]
752
+ "input_ids": input_ids, # [bsz, prompt_length + response_length]
753
+ "attention_mask": attention_mask, # [bsz, prompt_length + response_length]
754
+ # position_ids: [bsz, 3, prompt_length + response_length] or [bsz, prompt_length + response_length]
755
+ "position_ids": position_ids,
756
+ **optional_outputs,
757
+ },
758
+ batch_size=len(inputs),
759
+ )
760
+
761
+ scores = [input.reward_score for input in inputs]
762
+ if all(score is not None for score in scores):
763
+ prompt_length = prompt_ids.size(1)
764
+ response_length = attention_mask[:, prompt_length:].sum(dim=1) - 1
765
+ rm_scores = torch.zeros_like(response_mask, dtype=torch.float32)
766
+ rm_scores[torch.arange(response_mask.size(0)), response_length] = torch.tensor(scores, dtype=torch.float32)
767
+ batch["rm_scores"] = rm_scores
768
+
769
+ non_tensor_batch = {
770
+ "__num_turns__": np.array([input.num_turns for input in inputs], dtype=np.int32),
771
+ }
772
+
773
+ # add reward_extra_info to non_tensor_batch
774
+ reward_extra_infos = [input.extra_fields.get("reward_extra_info", {}) for input in inputs]
775
+ reward_extra_keys = list(reward_extra_infos[0].keys())
776
+ for key in reward_extra_keys:
777
+ non_tensor_batch[key] = np.array([info[key] for info in reward_extra_infos])
778
+
779
+ # Add multi_modal_inputs to non_tensor_batch if any samples have them
780
+ multi_modal_inputs_list = [input.multi_modal_inputs for input in inputs]
781
+ if any(mmi is not None for mmi in multi_modal_inputs_list):
782
+ non_tensor_batch["multi_modal_inputs"] = np.array(multi_modal_inputs_list, dtype=object)
783
+
784
+ metrics = [input.metrics.model_dump() for input in inputs]
785
+ # Collect extra fields from all inputs and convert them to np.ndarray
786
+ extra_fields = {}
787
+ all_keys = set(key for input_item in inputs for key in input_item.extra_fields)
788
+ for key in all_keys:
789
+ temp_arr = np.empty(len(inputs), dtype=object)
790
+ temp_arr[:] = [input.extra_fields.get(key) for input in inputs]
791
+ extra_fields[key] = temp_arr
792
+
793
+ non_tensor_batch.update(extra_fields)
794
+
795
+ # Only include reward_extra_keys in meta_info if rm_scores is in batch
796
+ # This avoids conflicts when reward_tensor is merged later in ray_trainer.py
797
+ if "rm_scores" in batch.keys():
798
+ meta_info = {"metrics": metrics, "reward_extra_keys": reward_extra_keys}
799
+ else:
800
+ meta_info = {"metrics": metrics}
801
+
802
+ return DataProto(
803
+ batch=batch,
804
+ non_tensor_batch=non_tensor_batch,
805
+ meta_info=meta_info,
806
+ )
807
+
808
+ def create_transferqueue_client(
809
+ self,
810
+ ):
811
+ """Create a client for data system (TransferQueue)."""
812
+ from verl.single_controller.ray.base import get_random_string
813
+ from verl.utils.transferqueue_utils import create_transferqueue_client
814
+
815
+ client_name = get_random_string(length=6)
816
+
817
+ self.tq_client = create_transferqueue_client(
818
+ client_id=f"AgentLoopWorker_{client_name}",
819
+ config=self.config.transfer_queue,
820
+ )
821
+
822
+
823
+ async def get_trajectory_info(step, index, validate):
824
+ """Get trajectory info.
825
+
826
+ Args:
827
+ step (int): global steps in the trainer.
828
+ index (list): form datastore extra_info.index column.
829
+ validate (bool): whether is a validate step.
830
+
831
+ Returns:
832
+ list: trajectory.
833
+ """
834
+ trajectory_info = []
835
+ rollout_n = 0
836
+ for i in range(len(index)):
837
+ if i > 0 and index[i - 1] == index[i]:
838
+ rollout_n += 1
839
+ else:
840
+ rollout_n = 0
841
+ trajectory_info.append({"step": step, "sample_index": index[i], "rollout_n": rollout_n, "validate": validate})
842
+ return trajectory_info
843
+
844
+
845
+ class AgentLoopManager:
846
+ """Agent loop manager that manages a group of agent loop workers."""
847
+
848
+ def __init__(
849
+ self,
850
+ config: DictConfig,
851
+ worker_group: RayWorkerGroup = None,
852
+ rollout_resource_pool: RayResourcePool = None,
853
+ rm_resource_pool: RayResourcePool = None,
854
+ ):
855
+ """Initialize agent loop manager.
856
+
857
+ Args:
858
+ config (DictConfig): trainer config.
859
+ worker_group (RayWorkerGroup): ActorRolloutRef worker group for hybrid mode; None for standalone mode.
860
+ rollout_resource_pool (RayResourcePool): Resource pool for actor rollout (Colocate or Standalone mode).
861
+ rm_resource_pool (RayResourcePool): Resource pool for reward model (Standalone mode).
862
+ """
863
+ self.config = config
864
+ self.worker_group = worker_group
865
+ self.reward_model_manager = None
866
+ self.reward_router_address = None
867
+ if self.config.reward_model.enable and self.config.reward_model.enable_resource_pool:
868
+ from verl.experimental.reward_loop import RewardModelManager
869
+
870
+ self.reward_model_manager = RewardModelManager(config.reward_model, rm_resource_pool)
871
+ self.reward_router_address = self.reward_model_manager.get_router_address()
872
+
873
+ # for recipe to change
874
+ if not hasattr(self, "rollout_replica_class"):
875
+ self.rollout_replica_class = get_rollout_replica_class(self.config.actor_rollout_ref.rollout.name)
876
+ if not hasattr(self, "agent_loop_workers_class"):
877
+ self.agent_loop_workers_class = ray.remote(AgentLoopWorker)
878
+
879
+ self._initialize_llm_servers(rollout_resource_pool)
880
+ self._init_agent_loop_workers()
881
+
882
+ def _initialize_llm_servers(self, rollout_resource_pool: RayResourcePool):
883
+ rollout_world_size = (
884
+ self.config.actor_rollout_ref.rollout.tensor_model_parallel_size
885
+ * self.config.actor_rollout_ref.rollout.data_parallel_size
886
+ * self.config.actor_rollout_ref.rollout.pipeline_model_parallel_size
887
+ )
888
+ world_size = (
889
+ self.worker_group.world_size
890
+ if self.worker_group
891
+ else self.config.trainer.n_gpus_per_node * self.config.trainer.nnodes
892
+ )
893
+ num_replicas = world_size // rollout_world_size
894
+
895
+ rollout_config = self.config.actor_rollout_ref.rollout
896
+ model_config = self.config.actor_rollout_ref.model
897
+ self.rollout_replicas = [
898
+ self.rollout_replica_class(
899
+ replica_rank=replica_rank,
900
+ config=rollout_config,
901
+ model_config=model_config,
902
+ gpus_per_node=self.config.trainer.n_gpus_per_node,
903
+ )
904
+ for replica_rank in range(num_replicas)
905
+ ]
906
+
907
+ if self.worker_group and rollout_config.name != "trtllm":
908
+ self._run_all([server.init_hybrid(self.worker_group) for server in self.rollout_replicas])
909
+ elif self.worker_group and rollout_config.name == "trtllm":
910
+ self._run_all(
911
+ [
912
+ server.init_hybrid_colocated(self.worker_group, rollout_resource_pool)
913
+ for server in self.rollout_replicas
914
+ ]
915
+ )
916
+ else:
917
+ self._run_all([server.init_standalone() for server in self.rollout_replicas])
918
+
919
+ self.server_handles = [server._server_handle for server in self.rollout_replicas]
920
+ self.server_addresses = [server._server_address for server in self.rollout_replicas]
921
+
922
+ print(f"AgentLoopManager: {self.server_addresses}")
923
+
924
+ # Update Prometheus configuration with server addresses
925
+ if rollout_config.prometheus.enable:
926
+ if rollout_config.disable_log_stats:
927
+ raise ValueError("PROMETHEUS needs disable_log_stats==False, but it is currently True.")
928
+ update_prometheus_config(rollout_config.prometheus, self.server_addresses, rollout_config.name)
929
+
930
+ def _init_agent_loop_workers(self):
931
+ self.agent_loop_workers = []
932
+ num_workers = self.config.actor_rollout_ref.rollout.agent.num_workers
933
+
934
+ node_ids = [node["NodeID"] for node in ray.nodes() if node["Alive"] and node["Resources"].get("CPU", 0) > 0]
935
+ for i in range(num_workers):
936
+ # Round-robin scheduling over the all nodes
937
+ node_id = node_ids[i % len(node_ids)]
938
+ self.agent_loop_workers.append(
939
+ self.agent_loop_workers_class.options(
940
+ name=f"agent_loop_worker_{i}" + f"_{uuid4().hex[:8]}",
941
+ scheduling_strategy=ray.util.scheduling_strategies.NodeAffinitySchedulingStrategy(
942
+ node_id=node_id, soft=True
943
+ ),
944
+ ).remote(self.config, self.server_handles, self.reward_router_address)
945
+ )
946
+
947
+ def generate_sequences(self, prompts: DataProto) -> DataProto:
948
+ """Split input batch and dispatch to agent loop workers.
949
+
950
+ Args:
951
+ prompts (DataProto): Input batch.
952
+
953
+ Returns:
954
+ DataProto: Output batch.
955
+ """
956
+
957
+ # TODO: move reward_model_manager out of agent_loop manager
958
+ if self.reward_model_manager:
959
+ self.reward_model_manager.wake_up()
960
+
961
+ chunkes = prompts.chunk(len(self.agent_loop_workers))
962
+ outputs = ray.get(
963
+ [
964
+ worker.generate_sequences.remote(chunk)
965
+ for worker, chunk in zip(self.agent_loop_workers, chunkes, strict=True)
966
+ ]
967
+ )
968
+ output = DataProto.concat(outputs)
969
+ if self.reward_model_manager:
970
+ self.reward_model_manager.sleep()
971
+
972
+ # calculate performance metrics
973
+ metrics = [output.meta_info.pop("metrics") for output in outputs] # List[List[Dict[str, str]]]
974
+ timing = self._performance_metrics(metrics, output)
975
+
976
+ output.meta_info = {"timing": timing, **outputs[0].meta_info}
977
+ return output
978
+
979
+ def _performance_metrics(self, metrics: list[list[dict[str, str]]], output: DataProto) -> dict[str, float]:
980
+ timing = {}
981
+ t_generate_sequences = np.array([metric["generate_sequences"] for chunk in metrics for metric in chunk])
982
+ t_tool_calls = np.array([metric["tool_calls"] for chunk in metrics for metric in chunk])
983
+ num_preempted = np.array([metric["num_preempted"] for chunk in metrics for metric in chunk])
984
+ timing["agent_loop/num_preempted/min"] = num_preempted.min()
985
+ timing["agent_loop/num_preempted/max"] = num_preempted.max()
986
+ timing["agent_loop/num_preempted/mean"] = num_preempted.mean()
987
+ timing["agent_loop/generate_sequences/min"] = t_generate_sequences.min()
988
+ timing["agent_loop/generate_sequences/max"] = t_generate_sequences.max()
989
+ timing["agent_loop/generate_sequences/mean"] = t_generate_sequences.mean()
990
+ timing["agent_loop/tool_calls/min"] = t_tool_calls.min()
991
+ timing["agent_loop/tool_calls/max"] = t_tool_calls.max()
992
+ timing["agent_loop/tool_calls/mean"] = t_tool_calls.mean()
993
+
994
+ # batch sequence generation is bounded by the slowest sample
995
+ slowest = np.argmax(t_generate_sequences + t_tool_calls)
996
+ attention_mask = output.batch["attention_mask"][slowest]
997
+ prompt_length = output.batch["prompts"].shape[1]
998
+ timing["agent_loop/slowest/generate_sequences"] = t_generate_sequences[slowest]
999
+ timing["agent_loop/slowest/tool_calls"] = t_tool_calls[slowest]
1000
+ timing["agent_loop/slowest/prompt_length"] = attention_mask[:prompt_length].sum().item()
1001
+ timing["agent_loop/slowest/response_length"] = attention_mask[prompt_length:].sum().item()
1002
+ timing["agent_loop/slowest/num_preempted"] = num_preempted[slowest]
1003
+
1004
+ return timing
1005
+
1006
+ def clear_kv_cache(self):
1007
+ """Clear all rollout kv cache, but don`t sleep."""
1008
+ self._run_all([replica.clear_kv_cache() for replica in self.rollout_replicas])
1009
+
1010
+ def start_profile(self, **kwargs):
1011
+ """Start profiling on all rollout replicas."""
1012
+ self._run_all([replica.start_profile(**kwargs) for replica in self.rollout_replicas])
1013
+
1014
+ def stop_profile(self):
1015
+ """Stop profiling on all rollout replicas."""
1016
+ self._run_all([replica.stop_profile() for replica in self.rollout_replicas])
1017
+
1018
+ def _run_all(self, tasks: list[asyncio.Task]):
1019
+ async def run_all():
1020
+ await asyncio.gather(*tasks)
1021
+
1022
+ asyncio.run(run_all())
code/RL_model/verl/verl_train/verl/experimental/agent_loop/prometheus_utils.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import logging
17
+ import os
18
+
19
+ import ray
20
+ import yaml
21
+
22
+ from verl.workers.config.rollout import PrometheusConfig
23
+
24
+ logger = logging.getLogger(__file__)
25
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
26
+
27
+
28
+ def update_prometheus_config(config: PrometheusConfig, server_addresses: list[str], rollout_name: str | None = None):
29
+ """
30
+ Update Prometheus configuration file with server addresses and reload on first node.
31
+
32
+ server_addresses: vllm or sglang server addresses
33
+
34
+ rollout_name: name of the rollout backend (e.g., "vllm", "sglang")
35
+ """
36
+
37
+ if not server_addresses:
38
+ logger.warning("No server addresses available to update Prometheus config")
39
+ return
40
+
41
+ try:
42
+ # Get Prometheus config file path from environment or use default
43
+ prometheus_config_json = {
44
+ "global": {"scrape_interval": "10s", "evaluation_interval": "10s"},
45
+ "scrape_configs": [
46
+ {
47
+ "job_name": "ray",
48
+ "file_sd_configs": [{"files": ["/tmp/ray/prom_metrics_service_discovery.json"]}],
49
+ },
50
+ {"job_name": "rollout", "static_configs": [{"targets": server_addresses}]},
51
+ ],
52
+ }
53
+
54
+ # Write configuration file to all nodes
55
+ @ray.remote(num_cpus=0)
56
+ def write_config_file(config_data, config_path):
57
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
58
+ with open(config_path, "w") as f:
59
+ yaml.dump(config_data, f, default_flow_style=False, indent=2)
60
+ return True
61
+
62
+ # Reload Prometheus on all nodes. Only master node should succeed, skip errors on other nodes.
63
+ @ray.remote(num_cpus=0)
64
+ def reload_prometheus(port):
65
+ import socket
66
+ import subprocess
67
+
68
+ hostname = socket.gethostname()
69
+ ip_address = socket.gethostbyname(hostname)
70
+
71
+ reload_url = f"http://{ip_address}:{port}/-/reload"
72
+
73
+ try:
74
+ subprocess.run(["curl", "-X", "POST", reload_url], capture_output=True, text=True, timeout=10)
75
+ print(f"Reloading Prometheus on node: {reload_url}")
76
+ except Exception:
77
+ # Skip errors on non-master nodes
78
+ pass
79
+
80
+ # Get all available nodes and schedule tasks on each node
81
+ nodes = ray.nodes()
82
+ alive_nodes = [node for node in nodes if node["Alive"]]
83
+
84
+ # Write config files on all nodes
85
+ write_tasks = []
86
+ for node in alive_nodes:
87
+ node_ip = node["NodeManagerAddress"]
88
+ task = write_config_file.options(
89
+ resources={"node:" + node_ip: 0.001} # Schedule to specific node
90
+ ).remote(prometheus_config_json, config.file)
91
+ write_tasks.append(task)
92
+
93
+ ray.get(write_tasks)
94
+
95
+ server_type = rollout_name.upper() if rollout_name else "rollout"
96
+ print(f"Updated Prometheus configuration at {config.file} with {len(server_addresses)} {server_type} servers")
97
+
98
+ # Reload Prometheus on all nodes
99
+ reload_tasks = []
100
+ for node in alive_nodes:
101
+ node_ip = node["NodeManagerAddress"]
102
+ task = reload_prometheus.options(
103
+ resources={"node:" + node_ip: 0.001} # Schedule to specific node
104
+ ).remote(config.port)
105
+ reload_tasks.append(task)
106
+
107
+ ray.get(reload_tasks)
108
+
109
+ except Exception as e:
110
+ logger.error(f"Failed to update Prometheus configuration: {e}")
code/RL_model/verl/verl_train/verl/experimental/agent_loop/single_turn_agent_loop.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import logging
15
+ import os
16
+ from typing import Any
17
+ from uuid import uuid4
18
+
19
+ from verl.experimental.agent_loop.agent_loop import AgentLoopBase, AgentLoopOutput, register
20
+ from verl.tools.utils.tool_registry import initialize_tools_from_config
21
+ from verl.utils.profiler import simple_timer
22
+
23
+ logger = logging.getLogger(__file__)
24
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
25
+
26
+
27
+ @register("single_turn_agent")
28
+ class SingleTurnAgentLoop(AgentLoopBase):
29
+ """Naive agent loop that only do single turn chat completion."""
30
+
31
+ def __init__(self, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+ self.prompt_length = self.config.actor_rollout_ref.rollout.prompt_length
34
+ self.response_length = self.config.actor_rollout_ref.rollout.response_length
35
+
36
+ tool_config_path = self.config.data.tool_config_path
37
+ tool_list = initialize_tools_from_config(tool_config_path) if tool_config_path else []
38
+ self.tool_schemas = [tool.tool_schema.model_dump(exclude_unset=True, exclude_none=True) for tool in tool_list]
39
+
40
+ async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
41
+ messages = list(kwargs["raw_prompt"])
42
+
43
+ # 1. extract images and videos from messages
44
+ multi_modal_data = await self.process_vision_info(messages)
45
+ images = multi_modal_data.get("images")
46
+ videos = multi_modal_data.get("videos")
47
+
48
+ # 2. apply chat template and tokenize
49
+ prompt_ids = await self.apply_chat_template(
50
+ messages,
51
+ tools=self.tool_schemas,
52
+ images=images,
53
+ videos=videos,
54
+ )
55
+
56
+ # 3. generate sequences
57
+ metrics = {}
58
+ with simple_timer("generate_sequences", metrics):
59
+ output = await self.server_manager.generate(
60
+ request_id=uuid4().hex,
61
+ prompt_ids=prompt_ids,
62
+ sampling_params=sampling_params,
63
+ image_data=images,
64
+ video_data=videos,
65
+ )
66
+ if metrics.get("num_preempted") is None:
67
+ metrics["num_preempted"] = output.num_preempted if output.num_preempted is not None else -1
68
+ response_mask = [1] * len(output.token_ids)
69
+
70
+ output = AgentLoopOutput(
71
+ prompt_ids=prompt_ids,
72
+ response_ids=output.token_ids[: self.response_length],
73
+ response_mask=response_mask[: self.response_length],
74
+ response_logprobs=output.log_probs[: self.response_length] if output.log_probs else None,
75
+ routed_experts=(
76
+ output.routed_experts[: len(prompt_ids) + self.response_length]
77
+ if output.routed_experts is not None
78
+ else None
79
+ ),
80
+ multi_modal_data=multi_modal_data,
81
+ num_turns=2,
82
+ metrics=metrics,
83
+ )
84
+ return output
code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_agent_loop.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import json
16
+ import logging
17
+ import os
18
+ from enum import Enum
19
+ from typing import Any, Optional
20
+ from uuid import uuid4
21
+
22
+ import torch
23
+ from PIL import Image
24
+ from transformers import AutoProcessor, AutoTokenizer
25
+
26
+ from verl.experimental.agent_loop.agent_loop import (
27
+ AgentLoopBase,
28
+ AgentLoopOutput,
29
+ AsyncLLMServerManager,
30
+ DictConfigWrap,
31
+ register,
32
+ )
33
+ from verl.experimental.agent_loop.tool_parser import FunctionCall, ToolParser
34
+ from verl.experimental.agent_loop.utils import build_gpt_oss_tool_response_text
35
+ from verl.interactions.base import BaseInteraction
36
+ from verl.interactions.utils.interaction_registry import initialize_interactions_from_config
37
+ from verl.tools.schemas import ToolResponse
38
+ from verl.tools.utils.tool_registry import initialize_tools_from_config
39
+ from verl.utils.profiler import simple_timer
40
+ from verl.utils.rollout_trace import rollout_trace_op
41
+
42
+ logger = logging.getLogger(__file__)
43
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
44
+
45
+
46
+ class AgentState(Enum):
47
+ PENDING = "pending"
48
+ GENERATING = "generating"
49
+ PROCESSING_TOOLS = "processing_tools"
50
+ TERMINATED = "terminated"
51
+ INTERACTING = "interacting"
52
+
53
+
54
+ class AgentData:
55
+ """Encapsulates all state variables for the agent loop. AgentData is passed to tool calling in case that
56
+ tool may need to access full history state. User can store any tool session data in `extra_fields`."""
57
+
58
+ def __init__(
59
+ self,
60
+ messages: list[dict[str, Any]],
61
+ image_data: list[Image.Image],
62
+ video_data: list[tuple[torch.Tensor, dict[str, Any]]],
63
+ metrics: dict[str, Any],
64
+ request_id: str,
65
+ tools_kwargs: dict[str, Any],
66
+ interaction: Optional[BaseInteraction] = None,
67
+ interaction_kwargs: Optional[dict[str, Any]] = None,
68
+ ):
69
+ self.messages = messages
70
+ self.image_data = image_data
71
+ self.video_data = video_data
72
+ self.metrics = metrics
73
+ self.request_id = request_id
74
+ self.tools_kwargs = tools_kwargs
75
+ self.interaction = interaction
76
+ self.interaction_kwargs = interaction_kwargs or {}
77
+
78
+ # State variables
79
+ self.prompt_ids: list[int] = []
80
+ self.response_ids: list[int] = []
81
+ self.response_mask: list[int] = []
82
+ self.response_logprobs: list[float] = []
83
+ self.turn_scores: list[float] = []
84
+ self.tool_rewards: list[float] = []
85
+ self.user_turns = 0
86
+ self.assistant_turns = 0
87
+
88
+ # Temporary state for tool calls
89
+ self.tool_calls: list[FunctionCall] = []
90
+
91
+ # Extra fields for dynamic addition, e.g., tool session data
92
+ self.extra_fields: dict[str, Any] = {}
93
+
94
+
95
+ @register("tool_agent")
96
+ class ToolAgentLoop(AgentLoopBase):
97
+ def __init__(
98
+ self,
99
+ trainer_config: DictConfigWrap,
100
+ server_manager: AsyncLLMServerManager,
101
+ tokenizer: AutoTokenizer,
102
+ processor: AutoProcessor,
103
+ **kwargs,
104
+ ):
105
+ super().__init__(trainer_config, server_manager, tokenizer, processor, **kwargs)
106
+ config = trainer_config.config
107
+
108
+ # Initialize tools from config file
109
+ self.max_user_turns = config.actor_rollout_ref.rollout.multi_turn.max_user_turns
110
+ self.max_assistant_turns = config.actor_rollout_ref.rollout.multi_turn.max_assistant_turns
111
+ self.max_parallel_calls = config.actor_rollout_ref.rollout.multi_turn.max_parallel_calls
112
+ self.max_tool_response_length = config.actor_rollout_ref.rollout.multi_turn.max_tool_response_length
113
+ self.tool_response_truncate_side = config.actor_rollout_ref.rollout.multi_turn.tool_response_truncate_side
114
+ tool_config_path = config.actor_rollout_ref.rollout.multi_turn.tool_config_path
115
+ tool_list = initialize_tools_from_config(tool_config_path) if tool_config_path else []
116
+ self.tools = {tool.name: tool for tool in tool_list}
117
+ self.tool_schemas = [tool.tool_schema.model_dump(exclude_unset=True, exclude_none=True) for tool in tool_list]
118
+ self.tool_parser = ToolParser.get_tool_parser(
119
+ config.actor_rollout_ref.rollout.multi_turn.format, self.tokenizer
120
+ )
121
+ self.tool_parser_name = config.actor_rollout_ref.rollout.multi_turn.format
122
+
123
+ self.prompt_length = config.actor_rollout_ref.rollout.prompt_length
124
+ self.response_length = config.actor_rollout_ref.rollout.response_length
125
+
126
+ # Initialize interactions from config file
127
+ self.interaction_config_file = config.actor_rollout_ref.rollout.multi_turn.interaction_config_path
128
+ if self.interaction_config_file:
129
+ self.interaction_map: dict[str, BaseInteraction] = self._initialize_interactions(
130
+ self.interaction_config_file
131
+ )
132
+
133
+ @rollout_trace_op
134
+ async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
135
+ messages = list(kwargs["raw_prompt"])
136
+
137
+ # extract images and videos from messages
138
+ multi_modal_data = await self.process_vision_info(messages)
139
+ images = multi_modal_data.get("images")
140
+ videos = multi_modal_data.get("videos")
141
+
142
+ metrics = {}
143
+ request_id = uuid4().hex
144
+ tools_kwargs = kwargs.get("tools_kwargs", {})
145
+
146
+ # Initialize interaction if needed
147
+ interaction = None
148
+ interaction_kwargs = {}
149
+ if self.interaction_config_file:
150
+ interaction_kwargs = kwargs["extra_info"]["interaction_kwargs"]
151
+ if "name" not in interaction_kwargs:
152
+ raise ValueError("'name' key is required in interaction_kwargs")
153
+ interaction_name = interaction_kwargs["name"]
154
+ if interaction_name not in self.interaction_map:
155
+ raise ValueError(
156
+ f"Interaction '{interaction_name}' not found in interaction_map. Available interactions: "
157
+ f"{list(self.interaction_map.keys())}"
158
+ )
159
+ interaction = self.interaction_map[interaction_name]
160
+ await interaction.start_interaction(request_id, **interaction_kwargs)
161
+ # Create AgentData instance to encapsulate all state
162
+ agent_data = AgentData(
163
+ messages=messages,
164
+ image_data=images,
165
+ video_data=videos,
166
+ metrics=metrics,
167
+ request_id=request_id,
168
+ tools_kwargs=tools_kwargs,
169
+ interaction=interaction,
170
+ interaction_kwargs=interaction_kwargs,
171
+ )
172
+
173
+ # State machine loop
174
+ state = AgentState.PENDING
175
+ while state != AgentState.TERMINATED:
176
+ if state == AgentState.PENDING:
177
+ state = await self._handle_pending_state(agent_data, sampling_params)
178
+ elif state == AgentState.GENERATING:
179
+ state = await self._handle_generating_state(agent_data, sampling_params)
180
+ elif state == AgentState.PROCESSING_TOOLS:
181
+ state = await self._handle_processing_tools_state(agent_data)
182
+ elif state == AgentState.INTERACTING:
183
+ state = await self._handle_interacting_state(agent_data)
184
+ else:
185
+ logger.error(f"Invalid state: {state}")
186
+ state = AgentState.TERMINATED
187
+
188
+ # Finalize output
189
+ response_ids = agent_data.prompt_ids[-len(agent_data.response_mask) :]
190
+ prompt_ids = agent_data.prompt_ids[: len(agent_data.prompt_ids) - len(agent_data.response_mask)]
191
+ multi_modal_data = {}
192
+ if agent_data.image_data is not None:
193
+ multi_modal_data["images"] = agent_data.image_data
194
+ if agent_data.video_data is not None:
195
+ multi_modal_data["videos"] = agent_data.video_data
196
+ output = AgentLoopOutput(
197
+ prompt_ids=prompt_ids,
198
+ response_ids=response_ids[: self.response_length],
199
+ response_mask=agent_data.response_mask[: self.response_length],
200
+ multi_modal_data=multi_modal_data,
201
+ response_logprobs=agent_data.response_logprobs[: self.response_length]
202
+ if agent_data.response_logprobs
203
+ else None,
204
+ num_turns=agent_data.user_turns + agent_data.assistant_turns + 1,
205
+ metrics=agent_data.metrics,
206
+ extra_fields={},
207
+ )
208
+ output.extra_fields.update({"turn_scores": agent_data.turn_scores, "tool_rewards": agent_data.tool_rewards})
209
+ return output
210
+
211
+ async def _handle_pending_state(self, agent_data: AgentData, sampling_params: dict[str, Any]) -> AgentState:
212
+ """Handle the pending state: prepare the prompt and start generation."""
213
+ prompt_ids = await self.apply_chat_template(
214
+ agent_data.messages,
215
+ tools=self.tool_schemas,
216
+ images=agent_data.image_data,
217
+ videos=agent_data.video_data,
218
+ )
219
+ agent_data.prompt_ids = prompt_ids
220
+ return AgentState.GENERATING
221
+
222
+ async def _handle_generating_state(
223
+ self, agent_data: AgentData, sampling_params: dict[str, Any], ignore_termination: bool = False
224
+ ) -> AgentState:
225
+ """Handle the generating state: generate model response and check for tool calls."""
226
+ add_messages: list[dict[str, Any]] = []
227
+
228
+ with simple_timer("generate_sequences", agent_data.metrics):
229
+ output = await self.server_manager.generate(
230
+ request_id=agent_data.request_id,
231
+ prompt_ids=agent_data.prompt_ids,
232
+ sampling_params=sampling_params,
233
+ image_data=agent_data.image_data,
234
+ video_data=agent_data.video_data,
235
+ )
236
+ # first time to set num_preempted
237
+ if agent_data.metrics.get("num_preempted") is None:
238
+ agent_data.metrics["num_preempted"] = output.num_preempted if output.num_preempted is not None else -1
239
+ # then add num_preempted to the metrics
240
+ else:
241
+ agent_data.metrics["num_preempted"] += output.num_preempted if output.num_preempted is not None else 0
242
+
243
+ agent_data.assistant_turns += 1
244
+ agent_data.response_ids = output.token_ids
245
+ agent_data.prompt_ids += agent_data.response_ids
246
+ agent_data.response_mask += [1] * len(agent_data.response_ids)
247
+ if output.log_probs:
248
+ agent_data.response_logprobs += output.log_probs
249
+
250
+ if output.routed_experts is not None:
251
+ agent_data.routed_experts = output.routed_experts
252
+
253
+ # Check termination conditions
254
+ if not ignore_termination and len(agent_data.response_mask) >= self.response_length:
255
+ return AgentState.TERMINATED
256
+ if self.max_assistant_turns and agent_data.assistant_turns >= self.max_assistant_turns:
257
+ return AgentState.TERMINATED
258
+ if self.max_user_turns and agent_data.user_turns >= self.max_user_turns:
259
+ return AgentState.TERMINATED
260
+
261
+ # Extract tool calls
262
+ _, agent_data.tool_calls = await self.tool_parser.extract_tool_calls(agent_data.response_ids)
263
+
264
+ # Handle interaction if needed
265
+ if self.interaction_config_file:
266
+ assistant_message = await self.loop.run_in_executor(
267
+ None, lambda: self.tokenizer.decode(agent_data.response_ids, skip_special_tokens=True)
268
+ )
269
+ add_messages.append({"role": "assistant", "content": assistant_message})
270
+ agent_data.messages.extend(add_messages)
271
+
272
+ # Determine next state
273
+ if agent_data.tool_calls:
274
+ return AgentState.PROCESSING_TOOLS
275
+ elif self.interaction_config_file:
276
+ return AgentState.INTERACTING
277
+ else:
278
+ return AgentState.TERMINATED
279
+
280
+ async def _handle_processing_tools_state(self, agent_data: AgentData) -> AgentState:
281
+ """Handle the processing tools state: execute tool calls and prepare tool responses."""
282
+ add_messages: list[dict[str, Any]] = []
283
+ new_images_this_turn: list[Any] = [] # Local variable instead of agent_data attribute
284
+
285
+ tasks = []
286
+ tool_call_names = []
287
+ for tool_call in agent_data.tool_calls[: self.max_parallel_calls]:
288
+ tasks.append(self._call_tool(tool_call, agent_data.tools_kwargs, agent_data))
289
+ tool_call_names.append(tool_call.name)
290
+
291
+ with simple_timer("tool_calls", agent_data.metrics):
292
+ responses = await asyncio.gather(*tasks)
293
+
294
+ # Process tool responses and update multi_modal_data
295
+ # Removed: agent_data.new_images_this_turn = []
296
+ for tool_response, tool_reward, _ in responses:
297
+ # Create message from tool response
298
+ if tool_response.image or tool_response.video:
299
+ # Multi-modal content with structured format
300
+ if not getattr(self.processor, "image_processor", None):
301
+ raise ValueError(
302
+ "Multimedia data can only be processed by `processor`, but the processor is None. "
303
+ "This error is often caused if you are using a LLM model but your tool returns multimodal "
304
+ "data. Plase use a vlm as the base model."
305
+ )
306
+ content = []
307
+ if tool_response.image:
308
+ content.append({"type": "image"})
309
+ if tool_response.video:
310
+ content.append({"type": "video"})
311
+ if tool_response.text:
312
+ content.append({"type": "text", "text": tool_response.text})
313
+ message = {"role": "tool", "content": content}
314
+ else:
315
+ # Text-only content
316
+ message = {"role": "tool", "content": tool_response.text or ""}
317
+
318
+ add_messages.append(message)
319
+
320
+ # Handle image data
321
+ if tool_response.image:
322
+ # Add new image data
323
+ if isinstance(tool_response.image, list):
324
+ # Ensure all elements in the list are valid image objects
325
+ for img in tool_response.image:
326
+ if img is not None: # Add a check to ensure the image is not None
327
+ new_images_this_turn.append(img) # Using local variable
328
+ else:
329
+ # Ensure the image is not None
330
+ if tool_response.image is not None:
331
+ new_images_this_turn.append(tool_response.image) # Using local variable
332
+
333
+ # Handle video data
334
+ if tool_response.video:
335
+ # Currently not supported, raise informative error
336
+ logger.warning("Multimedia type 'video' is not currently supported. Only 'image' is supported.")
337
+ raise NotImplementedError(
338
+ "Multimedia type 'video' is not currently supported. Only 'image' is supported."
339
+ )
340
+
341
+ if tool_reward is not None:
342
+ agent_data.tool_rewards.append(tool_reward)
343
+
344
+ agent_data.messages.extend(add_messages)
345
+
346
+ if self.tool_parser_name == "gpt-oss":
347
+ logger.info("manually format tool responses for gpt-oss")
348
+ tool_response_text = build_gpt_oss_tool_response_text(add_messages, tool_call_names)
349
+ response_ids = await self.loop.run_in_executor(
350
+ None, lambda: self.tokenizer.encode(tool_response_text, add_special_tokens=False)
351
+ )
352
+ else:
353
+ response_ids = await self.apply_chat_template(
354
+ add_messages,
355
+ images=new_images_this_turn, # Using local variable
356
+ videos=None,
357
+ remove_system_prompt=True,
358
+ )
359
+
360
+ if len(agent_data.response_mask) + len(response_ids) >= self.response_length:
361
+ return AgentState.TERMINATED
362
+ # Update prompt_ids and response_mask
363
+
364
+ if new_images_this_turn:
365
+ if agent_data.image_data is None:
366
+ agent_data.image_data = []
367
+ elif not isinstance(agent_data.image_data, list):
368
+ agent_data.image_data = [agent_data.image_data]
369
+ for img in new_images_this_turn:
370
+ agent_data.image_data.append(img)
371
+
372
+ agent_data.prompt_ids += response_ids
373
+ agent_data.response_mask += [0] * len(response_ids)
374
+ if agent_data.response_logprobs:
375
+ agent_data.response_logprobs += [0.0] * len(response_ids)
376
+ agent_data.user_turns += 1
377
+ return AgentState.GENERATING
378
+
379
+ async def _handle_interacting_state(self, agent_data: AgentData) -> AgentState:
380
+ """Handle the interacting state: get user input from interaction."""
381
+ (
382
+ should_terminate_sequence,
383
+ interaction_responses,
384
+ reward,
385
+ metrics,
386
+ ) = await agent_data.interaction.generate_response(
387
+ agent_data.request_id, agent_data.messages, **agent_data.interaction_kwargs
388
+ )
389
+ agent_data.user_turns += 1
390
+
391
+ add_messages: list[dict[str, Any]] = [{"role": "user", "content": interaction_responses}]
392
+ agent_data.messages.extend(add_messages)
393
+
394
+ if reward is not None:
395
+ agent_data.turn_scores.append(reward)
396
+
397
+ # Update prompt with user responses (similar to _handle_processing_tools_state)
398
+ response_ids = await self.apply_chat_template(
399
+ add_messages,
400
+ remove_system_prompt=True,
401
+ )
402
+
403
+ # Update prompt_ids and response_mask
404
+ agent_data.prompt_ids += response_ids
405
+ agent_data.response_mask += [0] * len(response_ids)
406
+ if agent_data.response_logprobs:
407
+ agent_data.response_logprobs += [0.0] * len(response_ids)
408
+
409
+ # double check prompt
410
+ # Check termination condition
411
+ if should_terminate_sequence:
412
+ return AgentState.TERMINATED
413
+ else:
414
+ return AgentState.GENERATING
415
+
416
+ async def _call_tool(
417
+ self, tool_call: FunctionCall, tools_kwargs: dict[str, Any], agent_data: AgentData
418
+ ) -> tuple[ToolResponse, float, dict]:
419
+ """Call tool and return tool response."""
420
+ tool, instance_id = None, None
421
+ try:
422
+ # TODO: append malformed tool_call to the prompt: invalid function name or arguments
423
+ tool_name = tool_call.name
424
+ tool_args = json.loads(tool_call.arguments)
425
+ tool = self.tools[tool_name]
426
+ kwargs = tools_kwargs.get(tool_name, {})
427
+ instance_id, _ = await tool.create(create_kwargs=kwargs.get("create_kwargs", {}))
428
+ tool_execution_response, tool_reward, res = await tool.execute(
429
+ instance_id, tool_args, agent_data=agent_data
430
+ )
431
+ except Exception as e:
432
+ logger.warning(f"Error when executing tool: {e}")
433
+ return (
434
+ ToolResponse(
435
+ text=f"Error when executing tool: {e}",
436
+ ),
437
+ 0.0,
438
+ {},
439
+ )
440
+ finally:
441
+ if tool and instance_id:
442
+ await tool.release(instance_id)
443
+
444
+ tool_response_text = tool_execution_response.text
445
+ if tool_response_text and len(tool_response_text) > self.max_tool_response_length:
446
+ if self.tool_response_truncate_side == "left":
447
+ tool_response_text = tool_response_text[: self.max_tool_response_length] + "...(truncated)"
448
+ elif self.tool_response_truncate_side == "right":
449
+ tool_response_text = "(truncated)..." + tool_response_text[-self.max_tool_response_length :]
450
+ else:
451
+ length = self.max_tool_response_length // 2
452
+ tool_response_text = tool_response_text[:length] + "...(truncated)..." + tool_response_text[-length:]
453
+
454
+ # Create ToolResponse from tool execution result
455
+ tool_response_kwargs = {"text": tool_response_text}
456
+
457
+ # Add multimedia data if present
458
+ for attr_name in ["image", "video"]:
459
+ if hasattr(tool_execution_response, attr_name):
460
+ attr_value = getattr(tool_execution_response, attr_name)
461
+ if attr_value is not None:
462
+ tool_response_kwargs[attr_name] = attr_value
463
+
464
+ return ToolResponse(**tool_response_kwargs), tool_reward, res
465
+
466
+ def _initialize_interactions(self, interaction_config_file):
467
+ """Initialize interactions from configuration.
468
+ Returns:
469
+ dict[str, BaseInteraction]: A dictionary mapping interaction names to interaction instances.
470
+ """
471
+ if interaction_config_file is None:
472
+ return {}
473
+
474
+ interaction_map = initialize_interactions_from_config(interaction_config_file)
475
+ return interaction_map
code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_parser.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import json
15
+ import logging
16
+ import os
17
+ from abc import ABC, abstractmethod
18
+
19
+ import regex
20
+ from pydantic import BaseModel
21
+
22
+ from verl.utils.ray_utils import get_event_loop
23
+ from verl.utils.rollout_trace import rollout_trace_op
24
+
25
+ logger = logging.getLogger(__file__)
26
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
27
+
28
+
29
+ class FunctionCall(BaseModel):
30
+ arguments: str
31
+ """
32
+ The arguments to call the function with, as generated by the model in JSON
33
+ format. Note that the model does not always generate valid JSON, and may
34
+ hallucinate parameters not defined by your function schema. Validate the
35
+ arguments in your code before calling your function.
36
+ """
37
+
38
+ name: str
39
+ """The name of the function to call."""
40
+
41
+
42
+ class ToolParser(ABC):
43
+ _registry: dict[str, type["ToolParser"]] = {}
44
+
45
+ def __init__(self, tokenizer) -> None:
46
+ self.tokenizer = tokenizer
47
+
48
+ @abstractmethod
49
+ async def extract_tool_calls(self, responses_ids: list[int]) -> tuple[str, list[FunctionCall]]:
50
+ """Extract tool calls from the responses.
51
+
52
+ Args:
53
+ responses_ids (List[int]): The ids of the responses.
54
+
55
+ Returns:
56
+ Tuple[str, List[FunctionCall]]: Content and extracted tool calls.
57
+ """
58
+ raise NotImplementedError
59
+
60
+ @classmethod
61
+ def get_tool_parser(cls, name: str, tokenizer):
62
+ if name not in cls._registry:
63
+ raise ValueError(f"Unknown tool parser: {name}")
64
+ return cls._registry[name](tokenizer)
65
+
66
+ @classmethod
67
+ def register(cls, name: str):
68
+ def decorator(subclass: type[ToolParser]) -> type[ToolParser]:
69
+ cls._registry[name] = subclass
70
+ return subclass
71
+
72
+ return decorator
73
+
74
+
75
+ @ToolParser.register("hermes")
76
+ class HermesToolParser(ToolParser):
77
+ """Adapted from https://github.com/vllm-project/vllm/blob/v0.9.1/vllm/entrypoints/openai/tool_parsers/hermes_tool_parser.py"""
78
+
79
+ def __init__(self, tokenizer) -> None:
80
+ super().__init__(tokenizer)
81
+
82
+ self.tool_call_start_token: str = "<tool_call>"
83
+ self.tool_call_end_token: str = "</tool_call>"
84
+ self.tool_call_regex = regex.compile(r"<tool_call>(.*?)</tool_call>", regex.DOTALL)
85
+
86
+ @rollout_trace_op
87
+ async def extract_tool_calls(self, responses_ids: list[int]) -> tuple[str, list[FunctionCall]]:
88
+ loop = get_event_loop()
89
+ text = await loop.run_in_executor(None, self.tokenizer.decode, responses_ids)
90
+ if self.tool_call_start_token not in text or self.tool_call_end_token not in text:
91
+ return text, []
92
+
93
+ matches = self.tool_call_regex.findall(text)
94
+ function_calls = []
95
+ for match in matches:
96
+ try:
97
+ function_call = json.loads(match)
98
+ name, arguments = function_call["name"], function_call["arguments"]
99
+ function_calls.append(FunctionCall(name=name, arguments=json.dumps(arguments, ensure_ascii=False)))
100
+ except Exception as e:
101
+ logger.error(f"Failed to decode tool call: {e}")
102
+
103
+ # remaing text exclude tool call tokens
104
+ content = self.tool_call_regex.sub("", text)
105
+
106
+ return content, function_calls
107
+
108
+
109
+ @ToolParser.register("gpt-oss")
110
+ class GptOssToolParser(ToolParser):
111
+ """
112
+ Tool parser for gpt-oss model.
113
+ Adapted from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/function_call/gpt_oss_detector.py
114
+
115
+ Args:
116
+ tokenizer: The tokenizer to use.
117
+ """
118
+
119
+ def __init__(self, tokenizer) -> None:
120
+ super().__init__(tokenizer)
121
+ # check https://cookbook.openai.com/articles/openai-harmony for more details.
122
+ self.cot_pattern = regex.compile(
123
+ r"<\|start\|>assistant<\|channel\|>analysis<\|message\|>.*?<\|end\|>", regex.DOTALL
124
+ )
125
+ # <|start|>assistant may be pre-appended in prompts, so we need to remove it.
126
+ self.partial_cot_pattern = regex.compile(r"<\|channel\|>analysis<\|message\|>(.*?)<\|end\|>", regex.DOTALL)
127
+ self.tool_call_pattern = regex.compile(
128
+ r"<\|start\|>assistant<\|channel\|>[^<]* to=functions\.([^<]+) "
129
+ r"<\|constrain\|>json<\|message\|>(.*?)<\|call\|>",
130
+ regex.DOTALL,
131
+ )
132
+
133
+ @rollout_trace_op
134
+ async def extract_tool_calls(self, responses_ids: list[int]) -> tuple[str, list[FunctionCall]]:
135
+ loop = get_event_loop()
136
+ # We need to keep special tokens for gpt-oss model for better tool call extraction.
137
+ text = await loop.run_in_executor(None, lambda: self.tokenizer.decode(responses_ids, skip_special_tokens=False))
138
+ # Need to remove padding tokens for better tool call extraction.
139
+ text = text.replace(self.tokenizer.pad_token, "")
140
+ # Need to reomve COT since COT may contain tool call tokens.But they are not valid tool calls.
141
+ text = regex.sub(self.cot_pattern, "", text)
142
+ text = regex.sub(self.partial_cot_pattern, "", text)
143
+
144
+ # check if there are tool calls in the text by re.findall
145
+ matches = regex.findall(self.tool_call_pattern, text)
146
+ if not matches:
147
+ return text, []
148
+
149
+ function_calls = []
150
+ for match in matches:
151
+ try:
152
+ name, arguments = match[0], match[1]
153
+ # don't check if arguments is valid JSON and leave it to client
154
+ function_calls.append(FunctionCall(name=name, arguments=arguments))
155
+ except Exception as e:
156
+ logger.error(f"Failed to decode tool call: {e}")
157
+
158
+ # remaing text exclude tool call tokens
159
+ content = regex.sub(self.tool_call_pattern, "", text)
160
+
161
+ return content, function_calls
code/RL_model/verl/verl_train/verl/experimental/agent_loop/utils.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ from typing import Any
17
+
18
+
19
+ def resolve_config_path(config_path: str) -> str:
20
+ """Resolve agent loop configuration file path.
21
+
22
+ In multi-node Ray training, relative paths may not resolve correctly
23
+ because the working directory on remote nodes can differ from the driver node.
24
+ This function resolves relative paths by checking multiple locations in order:
25
+ 1. If already absolute, return as-is
26
+ 2. Try current working directory
27
+ 3. Try relative to verl package installation (project root)
28
+
29
+ Args:
30
+ config_path: Configuration file path (relative or absolute)
31
+
32
+ Returns:
33
+ Absolute path to the configuration file
34
+
35
+ Raises:
36
+ FileNotFoundError: If the configuration file cannot be found
37
+ """
38
+ # Return absolute paths unchanged
39
+ if os.path.isabs(config_path):
40
+ return config_path
41
+
42
+ # Try current working directory first
43
+ cwd = os.path.abspath(os.getcwd())
44
+ cwd_path = os.path.abspath(os.path.join(cwd, config_path))
45
+ if (cwd_path == cwd or cwd_path.startswith(cwd + os.sep)) and os.path.exists(cwd_path):
46
+ return cwd_path
47
+
48
+ # Try relative to verl project root (where verl package is installed)
49
+ try:
50
+ import verl
51
+
52
+ verl_package_dir = os.path.abspath(os.path.dirname(verl.__file__))
53
+
54
+ # Strategy 1: For development/editable installs.
55
+ project_root = os.path.dirname(verl_package_dir)
56
+ dev_path = os.path.abspath(os.path.join(project_root, config_path))
57
+ if (dev_path == project_root or dev_path.startswith(project_root + os.sep)) and os.path.exists(dev_path):
58
+ return dev_path
59
+
60
+ # Strategy 2: For standard package installations.
61
+ install_path = os.path.abspath(os.path.join(verl_package_dir, config_path))
62
+ if (install_path == verl_package_dir or install_path.startswith(verl_package_dir + os.sep)) and os.path.exists(
63
+ install_path
64
+ ):
65
+ return install_path
66
+ except (ImportError, AttributeError):
67
+ pass # verl not installed or __file__ not available
68
+
69
+ # File not found - raise clear error
70
+ raise FileNotFoundError(
71
+ f"Agent loop configuration file not found: {config_path}. Tried current directory and verl project root."
72
+ )
73
+
74
+
75
+ # tokenizer.apply_chat_template is not working properly for gpt-oss model.
76
+ # Because the chat template requires tool call messages to parse tool response messages
77
+ # so we need to format the tool response manually.
78
+ def format_gpt_oss_tool_response_manually(tool_response: str, tool_call_name: str) -> str:
79
+ """Format tool response for gpt-oss model.
80
+ Args:
81
+ tool_response: Tool response string
82
+ tool_call_name: Name of the tool that was called
83
+
84
+ Returns:
85
+ Formatted tool response string
86
+ """
87
+ return f"<|start|>functions.{tool_call_name} to=assistant<|channel|>commentary<|message|>{tool_response}<|end|>"
88
+
89
+
90
+ def add_generation_prompt_for_gpt_oss(message_content: str) -> str:
91
+ """Add generation prompt for gpt-oss model.
92
+ Args:
93
+ message_content: Message content string
94
+
95
+ Returns:
96
+ Message content string with generation prompt
97
+ """
98
+ return message_content + "<|start|>assistant"
99
+
100
+
101
+ def build_gpt_oss_tool_response_text(messages: list[dict[str, Any]], tool_call_names: list[str]) -> str:
102
+ """Build gpt-oss tool response text (manual formatting + generation prompt)."""
103
+ tool_response_texts: list[str] = []
104
+ for i, tool_msg in enumerate(messages):
105
+ actual_tool_name = tool_call_names[i]
106
+ formatted = format_gpt_oss_tool_response_manually(tool_msg["content"], actual_tool_name)
107
+ tool_response_texts.append(formatted)
108
+ return add_generation_prompt_for_gpt_oss("".join(tool_response_texts))
code/RL_model/verl/verl_train/verl/experimental/dataset/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
code/RL_model/verl/verl_train/verl/experimental/dataset/sampler.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Amazon.com Inc and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from abc import abstractmethod
15
+ from collections.abc import Sized
16
+
17
+ from omegaconf import DictConfig
18
+ from torch.utils.data import Sampler
19
+
20
+ from verl import DataProto
21
+
22
+
23
+ class AbstractSampler(Sampler[int]):
24
+ """Abstract interface for custom samplers."""
25
+
26
+ @abstractmethod
27
+ def __init__(
28
+ self,
29
+ data_source: Sized,
30
+ data_config: DictConfig,
31
+ ):
32
+ pass
33
+
34
+
35
+ class AbstractCurriculumSampler(AbstractSampler):
36
+ """Experimental interface for curriculum learning samplers."""
37
+
38
+ @abstractmethod
39
+ def update(self, batch: DataProto) -> None:
40
+ pass
code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/dynamicgen_dataset.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Amazon.com Inc and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ Dataset class that enables dynamic data generation strategies between iterations of training.
16
+ This class extends RLHFDataset and uses an AbstractDataGen instance to generate data.
17
+
18
+ This is especially useful in settings where proposer model generates new tasks based
19
+ on rollout data.
20
+ """
21
+
22
+ import logging
23
+ from abc import ABC, abstractmethod
24
+ from typing import Optional
25
+
26
+ import datasets
27
+ from omegaconf import DictConfig
28
+ from torch.utils.data import Dataset
29
+ from transformers import PreTrainedTokenizer, ProcessorMixin
30
+
31
+ from verl import DataProto
32
+ from verl.utils.dataset import RLHFDataset
33
+ from verl.utils.import_utils import load_extern_object
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class AbstractDataGenerator(ABC):
39
+ def __init__(self, config: DictConfig):
40
+ self.config = config
41
+
42
+ @abstractmethod
43
+ def generate(self, dataset: Dataset) -> datasets.Dataset:
44
+ """
45
+ Generate method must be implemented by subclasses.
46
+ Args:
47
+ dataset: The dataset to generate from.
48
+ Returns:
49
+ Processed data or result as implemented by the subclass.
50
+ """
51
+ pass
52
+
53
+
54
+ class MockDataGenerator(AbstractDataGenerator):
55
+ """
56
+ A noop data gen class that only reappends the first datapoint.
57
+ This class is useful as a placeholder and testing.
58
+ """
59
+
60
+ def __init__(self, config: DictConfig = None):
61
+ super().__init__(config)
62
+
63
+ def generate(self, dataset: Dataset) -> datasets.Dataset:
64
+ print("MockDataGenerator: No operation performed on the dataset.")
65
+ return dataset.dataframe.select([0])
66
+
67
+
68
+ class DynamicGenDataset(RLHFDataset):
69
+ """
70
+ A dataset class that uses a data generation strategy to process data.
71
+ This class extends RLHFDataset and uses an AbstractDataGen instance to generate data.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ data_files: str | list[str],
77
+ tokenizer: PreTrainedTokenizer,
78
+ config: DictConfig,
79
+ processor: Optional[ProcessorMixin] = None,
80
+ ):
81
+ super().__init__(data_files, tokenizer, config, processor)
82
+ self.datagen: AbstractDataGenerator = config.datagen
83
+ assert "datagen" in config and config.datagen.get("path", None) is not None, (
84
+ f"datagen path is not set in config: {config}"
85
+ )
86
+ # Dynamically load the custom datagen class
87
+ datagen_cls = load_extern_object(config.datagen.path, config.datagen.name)
88
+
89
+ # Verify that the custom datagen class inherits from AbstractDataGenerator
90
+ abs_cls = AbstractDataGenerator
91
+ if not issubclass(datagen_cls, abs_cls):
92
+ raise TypeError(
93
+ f"The custom datagen class '{config.datagen.name}' from '{config.datagen.path}'"
94
+ + " must inherit from {abs_cls}"
95
+ )
96
+
97
+ self.data_generator = datagen_cls(config.datagen)
98
+ self.on_batch_end()
99
+
100
+ def append_dataframe(self, new_dataframe: datasets.Dataset):
101
+ new_dataframe = self.maybe_filter_out_long_prompts(new_dataframe)
102
+ self.dataframe = datasets.concatenate_datasets([self.dataframe, new_dataframe])
103
+
104
+ logger.info(f"new dataset len: {len(self.dataframe)}")
105
+
106
+ def on_batch_end(self, batch: DataProto) -> None:
107
+ """
108
+ Generate data using the provided data generation strategy.
109
+ Note: This method is intended to change the dataset after each training batch.
110
+ """
111
+ new_data = self.data_generator.generate(self)
112
+ self.append_dataframe(new_data)
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README.md ADDED
@@ -0,0 +1,599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Recipe: Fully Async Policy Trainer
2
+
3
+ **Author:** `https://github.com/meituan-search`
4
+
5
+ Last updated: 12/25/2025.
6
+
7
+ This document introduces a fully asynchronous PPO training system that completely decouples the Trainer and Rollouter,
8
+ supporting asynchronous sample generation and training.
9
+ Under this system, we achieved a 2.35x-2.67x performance improvement when training the Qwen2.5-7B model with 128 GPUs,
10
+ without significantly affecting the results.
11
+
12
+ ## Introduction
13
+
14
+ ### Background
15
+
16
+ The separated rollout and train architecture, compared to the colocate architecture, can allocate resources more
17
+ flexibly and design more flexible training logic, thereby addressing issues such as low GPU utilization and training
18
+ efficiency caused by long-tail problems.
19
+ The one_step_off_policy alleviates the problem of long rollout times and achieves some gains in training efficiency by
20
+ designing a separated architecture and performing asynchronous training between rollout and train for one round.
21
+ However, it forcibly uses data from one round of asynchronous training, which is not flexible enough and cannot
22
+ completely eliminate the impact of long-tail on training efficiency.
23
+ In other frameworks such as AReaL, Magistral, StreamRL, and AsyncFlow, asynchronous training and streaming training have
24
+ been implemented based on the separated architecture and have achieved gains.
25
+ We borrow from their methods and implemented them in VERL. The fully_async_policy supports asynchronous, streaming, and
26
+ partial
27
+ rollout training.
28
+ By reasonably setting parameters such as resource allocation and parameter synchronization frequency, fully_async_policy
29
+ can significantly improve training efficiency.
30
+
31
+ > Magistral https://arxiv.org/abs/2506.10910
32
+ >
33
+ > AReaL: A Large-Scale Asynchronous Reinforcement Learning System for Language
34
+ > Reasoning https://arxiv.org/abs/2505.24298
35
+ >
36
+ > StreamRL: Scalable, Heterogeneous, and Elastic RL for LLMs with Disaggregated Stream
37
+ > Generation https://arxiv.org/abs/2504.15930
38
+ >
39
+ > AsyncFlow: An Asynchronous Streaming RL Framework for Efficient LLM Post-Training https://arxiv.org/abs/2507.01663
40
+ >
41
+
42
+ ### Core Contributions
43
+
44
+ * **Resource Isolation**: Unlike using hybrid_engine, Rollouter and Trainer use separate computing resources and need to
45
+ specify the resources they occupy separately.
46
+ * **Parallel Generation and Training**: While the Trainer is training, the Rollouter is generating new samples.
47
+ * **Multi-step Asynchronous**: Compared to one step off policy, it supports asynchronous settings from 0.x steps to
48
+ multiple steps, making the asynchronous solution more flexible.
49
+ * **NCCL Parameter Synchronization**: Based on the nccl communication primitive, refer to [checkpoint-engine](https://github.com/MoonshotAI/checkpoint-engine) to
50
+ achieve efficient parameter synchronization between Rollouter and Trainer.
51
+ * **Stream Inference and Training**: Rollouter generates data sample by sample, and data transmission uses a single
52
+ sample as the minimum transmission unit.
53
+ * **Asynchronous Training and Freshness Control**: By setting the parameter async_training.staleness_threshold, it
54
+ supports training with samples generated by old parameters.
55
+ * **PartialRollout**: The Rollouter's inference process supports partial rollout logic. During parameter
56
+ synchronization, by adding `sleep() and resume()` logic, it
57
+ saves samples from ongoing rollouts and continues using them in the next rollout, reducing the time spent waiting for
58
+ ongoing tasks to finish during parameter synchronization.
59
+
60
+ Currently, the supported usage mode is megatron/fsdp+vllm. vllm must use the server mode based on AgentLoop.
61
+
62
+ ## Design
63
+
64
+ The overall architecture of fully_async_policy is shown in the figure below. fully_async_policy mainly consists of four
65
+ parts: Rollouter, MessageQueue, Trainer, and ParameterSynchronizer.
66
+
67
+ ![fully_async_policy_structure](
68
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_structure.svg?raw=true)
69
+
70
+ 1. Rollouter generates sequences sample by sample and puts the generated samples into the MessageQueue, with the
71
+ production speed controlled by freshness.
72
+ 2. MessageQueue is used to temporarily store samples generated by Rollouter.
73
+ 3. Trainer fetches samples from MessageQueue sample by sample. After fetching `require_batches*ppo_mini_batch_size`
74
+ samples, it will perform training. After training for async_training.trigger_parameter_sync_step rounds, it triggers
75
+ a parameter synchronization with Rollouter.
76
+ 4. ParameterSynchronizer implements the NCCL synchronous parameter synchronization capability.
77
+
78
+ The source of benefits compared to the base scheme lies in the fact that in the colocate case, using more resources for
79
+ rollout cannot solve the idleness caused by long-tail samples.
80
+ After we perform resource isolation, the time for rollout and train may be longer than before (because fewer resources
81
+ are used),
82
+ but the overlap in their time consumption reduces the end-to-end time consumption.
83
+
84
+ ![fully_async_policy_revenue](
85
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_revenue.svg?raw=true)
86
+
87
+ ## Usage
88
+
89
+ ### Parameter Description
90
+
91
+ | super params | implication |
92
+ |-----------------------------------------------|------------------------------------------------------------------------------------------------|
93
+ | `trainer.nnodes` | Number of nodes for Trainer |
94
+ | `trainer.n_gpus_per_node` | Number of GPUs per node for Trainer |
95
+ | `rollout.nnodes` | Number of nodes for Rollouter |
96
+ | `rollout.n_gpus_per_node` | Number of GPUs per node for Rollouter |
97
+ | `data.train_batch_size` | In the fully async strategy, this value is not effective (default is 0) |
98
+ | `data.gen_batch_size` | In the fully async strategy, uses streaming sample production logic (default is 1) |
99
+ | `rollout.total_rollout_steps` | Total number of rollout samples |
100
+ | `rollout.test_freq` | How many times Rollouter updates parameters before performing a validation |
101
+ | `actor_rollout_ref.actor.ppo_mini_batch_size` | The ppo_mini_batch_size is a global num across all workers/gpus |
102
+ | `async_training.require_batches` | Number of ppo_mini_batch_size that FullyAsyncTrainer fetches at once |
103
+ | `async_training.trigger_parameter_sync_step` | Indicates how many local updates FullyAsyncTrainer performs before a parameter synchronization |
104
+ | `async_training.staleness_threshold` | Freshness control |
105
+ | `async_training.partial_rollout` | Whether to perform partial_rollout |
106
+ | `async_training.use_rollout_log_probs` | Use log_probs generated by rollout |
107
+ | `async_training.compute_prox_log_prob` | Whether to compute log_prob using the training model's parameters during the training phase. | |
108
+ | `async_training.checkpoint_engine.enable`| Whether to use checkpoint_engine for accelerating, default `True`|
109
+ | `async_training.checkpoint_engine.overlap_broadcast_and_consume` | When use checkpoint_engine, whether to overlap broadcast and load_weights, default `False`|
110
+ | `async_training.checkpoint_engine.device_buffer_size_M` | When use checkpoint_engine, the user-specific bucket size (MB), default `4096`|
111
+ | `async_training.use_trainer_do_validate` | Whether use trainer node to do validate process, default `False`|
112
+
113
+ **Further Explanation:**
114
+
115
+ * `rollout.total_rollout_steps`
116
+
117
+ Compared to colocate, the quantity can be aligned by multiplying train_batch_size and step:
118
+ `rollout.total_rollout_steps = data.train_batch_size * step`.
119
+
120
+ * `async_training.trigger_parameter_sync_step`
121
+
122
+ In the fully async strategy, it indicates how many local updates the Trainer performs (i.e., how many times it fetches
123
+ `require_batches * ppo_mini_batch_size` samples) before a parameter synchronization with Rollouter.
124
+ Between every two parameter synchronizations between Rollouter and Trainer, the Trainer will process
125
+ `trigger_parameter_sync_step* require_batches*ppo_mini_batch_size` samples.
126
+ To fairly compare speed with colocate, `trigger_parameter_sync_step` should be set to
127
+ `data.train_batch_size / (require_batches * ppo_mini_batch_size)`.
128
+
129
+ * `async_training.staleness_threshold`
130
+
131
+ In the fully async strategy, it indicates the maximum proportion of stale samples allowed to be used.
132
+
133
+ * `staleness_threshold`=0, indicates synchronous training.
134
+ Rollouter will generate a fixed number of samples between two parameter updates, the sample count is:
135
+
136
+ `rollout_num = (trigger_parameter_sync_step*require_batches*ppo_mini_batch_size)`
137
+ * `staleness_threshold`>0, indicates asynchronous training, can be set to a decimal for more flexible asynchronous
138
+ calls.
139
+ Rollouter will generate at most the following number of samples between two parameter updates:
140
+
141
+ `rollout_num = (1+staleness_threshold)*(trigger_parameter_sync_step*require_batches*ppo_mini_batch_size) - num_staleness_sample`
142
+
143
+ `num_staleness_sample` represents the number of stale samples generated in excess during the last rollout.
144
+
145
+ Since it's a streaming system, rollout continues to generate and trainer continues to consume. If rollouter is slower,
146
+ trainer will trigger parameter synchronization earlier, and rollouter will not actually produce rollout_num samples.
147
+ When rollout is fast enough, setting `staleness_threshold` to 1 is basically equivalent to one_step_off policy.
148
+ To avoid too many expired samples affecting training accuracy, it is recommended to set this value to less than 1.
149
+
150
+ * `async_training.partial_rollout`
151
+
152
+ partial_rollout only actually takes effect when staleness_threshold>0.
153
+
154
+ * `async_training.use_rollout_log_probs`
155
+
156
+ In reinforcement learning algorithms, log_probs have implicit correlations with parameter versions and tokens. Due to
157
+ the settings of algorithms like PPO/GRPO/DAPO, when calculating importance sampling,
158
+ old_log_prob must use the log_probs corresponding to the rollout parameters and tokens to ensure algorithm
159
+ correctness. In the fully
160
+ async strategy, we default to old_log_prob being calculated by rollout rather than by trainer.
161
+
162
+ * `async_training.require_batches`
163
+
164
+ In streaming training, require_batches should be set to 1, indicating that training is performed after producing
165
+ enough ppo_mini_batch_size samples.
166
+ In actual testing, we found that if fewer samples are issued at once, due to the order of data distribution, it can
167
+ cause training instability and longer response lengths.
168
+ Here, we additionally provide require_batches for streaming distribution and control the number of samples
169
+ participating in training at once.
170
+
171
+ * `async_training.compute_prox_log_prob` (experimental)
172
+
173
+ During the training process, we observed that metrics and response lengths may become unstable in the later
174
+ stages of training. To mitigate this issue, we can use
175
+ the [Rollout Importance Sampling](https://verl.readthedocs.io/en/latest/advance/rollout_is.html)
176
+ technique for importance sampling. To utilize Rollout Importance Sampling, we need to compute log_prob using
177
+ the training engine, which requires enabling this switch.
178
+ Additionally, when compute_prox_log_prob and Rollout Importance Sampling are enabled under mode d
179
+ (async stream pipeline with partial rollout), our implementation approximates `Areal's Decoupled PPO`.
180
+
181
+ * `async_training.checkpoint_engine.enable`
182
+
183
+ Enabling the checkpoint engine generally reduces synchronization time overhead by more than 60% compared to
184
+ the original per-tensor parameter synchronization method. However, assembling buckets incurs additional
185
+ temporary GPU memory overhead.
186
+
187
+ * `async_training.checkpoint_engine.overlap_broadcast_and_consume`
188
+
189
+ Enabling pipeline between the broadcast and load_weights parameters will allocate additional GPU memory.
190
+ Since the main time consumption for parameter synchronization is not in the broadcast and load_weights phases,
191
+ but in the parameter generation phase (by megatron or FSDP), this option is off by default.
192
+
193
+ * `async_training.checkpoint_engine.device_buffer_size_M`
194
+
195
+ It controls the size of the memory buffer used for synchronization when the checkpoint-engine is enabled.
196
+ The actual `bucket_size` = `max(device_buffer_size_M, maximum parameter tensor size)`.
197
+ * When enable `overlap_broadcast_and_consume`, the additional device memory overhead of
198
+ trainer rank is `3 * bucket_size`and rollout rank is `2 * bucket_size`。
199
+ * When disable `overlap_broadcast_and_consume`, the additional device memory overhead of
200
+ trainer rank is `2 * bucket_size`and rollout rank is `1 * bucket_size`。
201
+
202
+ * `async_training.use_trainer_do_validate`
203
+
204
+ It controls whether to use the trainer's `do_validate` method for validation.
205
+ If set to True, the trainer will perform validation after each parameter update. It can reduce the validation time
206
+ overhead and trainer node idle time.
207
+ If set to False, the trainer will not perform validation.
208
+
209
+ ### Supported Modes
210
+
211
+ 1. on policy pipeline:
212
+ 1. **trigger_parameter_sync_step=1, staleness_threshold=0**
213
+ 2. Rollouter produces `require_batches*ppo_mini_batch_size` samples at once, Trainer fetches these samples for
214
+ training, and after training completes, Trainer and Rollouter perform a parameter synchronization;
215
+ 3. During the rollout phase, if there are long-tail samples but few rollout samples, shorter samples cannot fill
216
+ idle resources, causing some resource waste.
217
+ 4. As shown in figure a;
218
+
219
+ 2. stream off policy pipeline:
220
+ 1. **trigger_parameter_sync_step>1, staleness_threshold=0**
221
+ 2. Synchronous streaming training will be performed. Rollouter produces
222
+ `require_batches*ppo_mini_batch_size*trigger_parameter_sync_step` samples at once, Trainer performs a local
223
+ training every time it fetches `require_batches*ppo_mini_batch_size` samples, and after training
224
+ trigger_parameter_sync_step times, Trainer and Rollouter perform a parameter synchronization;
225
+ 3. Compared to a, since more samples are generated at once, resource idleness will be lower.
226
+ 4. In one step training, there will be two periods of resource idleness: when fetching the first batch of samples,
227
+ train waits for `require_batches*ppo_mini_batch_size` samples to be produced, and during the last parameter
228
+ update, rollout waits for training to complete.
229
+ 5. As shown in figure b;
230
+
231
+ 3. async stream pipeline with stale samples:
232
+ 1. **trigger_parameter_sync_step>=1, staleness_threshold>0, partial_rollout=False**
233
+ 2. After each parameter update, Rollouter will plan to produce at most rollout_num samples (in practice, the number
234
+ of samples generated may be less than this value depending on rollout speed).
235
+ 3. If the rollout process is relatively fast, Rollouter will generate some additional samples num_stale_samples
236
+ before parameter synchronization for immediate use by Trainer after synchronization.
237
+ When triggering parameter synchronization, if Rollouter has ongoing tasks, it will wait for the tasks to complete
238
+ and not add new tasks;
239
+ 4. Compared to b, except for the first step training, subsequent training will not have the time to wait for the
240
+ first batch rollout to finish, but will have the time to wait for active tasks to finish.
241
+ 5. As shown in figure c;
242
+
243
+ 4. async stream pipeline with partial rollout:
244
+ 1. **trigger_parameter_sync_step>=1, staleness_threshold>0, partial_rollout=True**
245
+ 2. Compared to c, when triggering parameter synchronization, if Rollouter has samples being produced, it will
246
+ interrupt the rollout process and perform parameter synchronization. The interrupted samples will continue to be
247
+ generated after synchronization. This reduces the time to wait for active tasks to finish.
248
+ 3. As shown in figure d;
249
+
250
+ ![fully_async_policy_mode](
251
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_mode.svg?raw=true)
252
+
253
+ ### Key Metrics
254
+
255
+ | metrics | implication |
256
+ |------------------------------------------------|--------------------------------------------------------------------------------------------------------|
257
+ | `trainer/idle_ratio` | Trainer idle rate |
258
+ | `rollouter/idle_ratio` | Rollouter idle rate |
259
+ | `fully_async/count/stale_samples_processed` | Total number of old samples used in training |
260
+ | `fully_async/count/stale_trajectory_processed` | Total number of old trajectories used in training (one sample produces rollout.n trajectories) |
261
+ | `fully_async/partial/total_partial_num` | Number of partial samples processed by Trainer between two trigger_parameter_sync_step |
262
+ | `fully_async/partial/partial_ratio` | Ratio of partial samples processed by Trainer between two trigger_parameter_sync_step |
263
+ | `fully_async/partial/max_partial_span` | Maximum parameter span of partial samples processed by Trainer between two trigger_parameter_sync_step |
264
+
265
+ ### Parameter Tuning Recommendations
266
+
267
+ * Resource Allocation and Adjustment:
268
+ * Reasonable resource allocation is the prerequisite for achieving good training efficiency. The ideal resource
269
+ allocation should make the rollout time and train time close, thereby minimizing pipeline bubbles in the entire
270
+ training process,
271
+ avoiding resource idleness, and ensuring Trainer does not use old samples. In real training scenarios, resource
272
+ allocation can be adjusted based on the idle time of rollout and train during actual training,
273
+ which can be obtained from rollouter/idle_ratio and trainer/idle_ratio. If rollouter/idle_ratio is high and
274
+ trainer/idle_ratio is low,
275
+ Trainer resources should be increased and Rollouter resources should be reduced, and vice versa.
276
+
277
+ * Key Parameters:
278
+ * staleness_threshold: Setting it too high will cause more old samples to be used, affecting model performance. It
279
+ is recommended to set it to less than 1.
280
+ * require_batches: The closer to 1, the closer to a pure streaming process, the smaller the training bubbles, and
281
+ the faster the acceleration effect that can be achieved in terms of speed, but it will affect the order of sample
282
+ processing;
283
+ * trigger_parameter_sync_step: The smaller the setting, the closer to on policy, but it will cause frequent
284
+ parameter synchronization. Long-tail samples waste resources that cannot be filled by short samples, resulting in
285
+ low resource utilization.
286
+ The larger the setting, the higher the computational efficiency, but the accuracy will be affected by off policy.
287
+ * rollout.test_freq: It will occupy Rollouter resources and is not recommended to be set too small.
288
+
289
+ * Mode Selection: By adjusting different parameters, the Fully Async architecture supports optimization acceleration at
290
+ different levels, suitable for tasks in different scenarios.
291
+ * For small-scale tasks that need to ensure training stability and on-policy nature, and have low speed
292
+ requirements, the on policy pipeline mode (Mode 1) can be tried.
293
+ * For scenarios that need to improve training throughput but are sensitive to staleness, the stream off policy
294
+ pipeline mode can be tried. That is, by
295
+ setting trigger_parameter_sync_step>1 to improve training efficiency, but still maintaining the synchronization
296
+ mechanism (staleness_threshold=0) (Mode 2).
297
+ * For large-scale tasks with high training speed requirements and can tolerate a certain degree of off-policy and
298
+ staleness, setting staleness_threshold>
299
+ 0 and partial_rollout=True can improve training efficiency, using the async stream pipeline mode (Mode 3 or 4).
300
+
301
+ ### Quick Start
302
+
303
+ ```shell
304
+ rollout_mode="async"
305
+ rollout_name="vllm" # sglang or vllm
306
+ if [ "$rollout_mode" = "async" ]; then
307
+ export VLLM_USE_V1=1
308
+ return_raw_chat="True"
309
+ fi
310
+
311
+ train_prompt_bsz=0
312
+ gen_prompt_bsz=1
313
+ n_resp_per_prompt=16
314
+ train_prompt_mini_bsz=32
315
+ total_rollout_steps=$(((512*400)))
316
+ test_freq=10
317
+ staleness_threshold=0
318
+ trigger_parameter_sync_step=16
319
+ partial_rollout=False
320
+
321
+
322
+ python -m recipe.fully_async_policy.fully_async_main \
323
+ train_batch_size=${train_prompt_bsz} \
324
+ data.gen_batch_size=${gen_prompt_bsz} \
325
+ data.return_raw_chat=${return_raw_chat} \
326
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
327
+ actor_rollout_ref.actor.strategy=fsdp2 \
328
+ critic.strategy=fsdp2 \
329
+ actor_rollout_ref.hybrid_engine=False \
330
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
331
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
332
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
333
+ actor_rollout_ref.rollout.name=${rollout_name} \
334
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
335
+ actor_rollout_ref.rollout.calculate_log_probs=True \
336
+ trainer.nnodes="${NNODES_TRAIN}" \
337
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
338
+ rollout.nnodes="${NNODES_ROLLOUT}" \
339
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
340
+ rollout.total_rollout_steps="${total_rollout_steps}" \
341
+ rollout.test_freq="${test_freq}" \
342
+ async_training.staleness_threshold="${staleness_threshold}" \
343
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
344
+ async_training.partial_rollout="${partial_rollout}"
345
+ ```
346
+
347
+ ## Experiments
348
+
349
+ ### Asynchronous Training on 7B Model
350
+
351
+ We used Qwen2.5-Math-7B to verify the benefits of the fully async strategy under long candidates and multiple resources.
352
+ Using the `async stream pipeline with stale samples` strategy, we achieved about 2x performance improvement on 32 cards,
353
+ 64 cards, and 128 cards without significantly affecting experimental results.
354
+
355
+ * Machine: H20
356
+ * Model: Qwen2.5-Math-7B
357
+ * Rollout length: max_response_length FSDP2: 28K tokens;
358
+ * Algorithm: DAPO
359
+ * Dataset: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
360
+ * Engine: vllm+FSDP2
361
+ * rollout.n: 16
362
+ * ppo_mini_batch_size: 32
363
+ * test_freq: 20
364
+
365
+ * colocate sync:
366
+ * step: 400
367
+ * train_batch_size: 512
368
+
369
+ * fully_async_policy
370
+ * total_rollout_steps: 512*400
371
+ * require_batches: 4
372
+ * trigger_parameter_sync_step: 4
373
+ * staleness_threshold: 0.5
374
+ * partial_rollout: True
375
+
376
+ | training mode | resource allocation | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
377
+ |:--------------------:|:---------------------:|:--------:|:--------:|:--------------:|:---------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:-------------------------------:|
378
+ | colocate sync | 32 | 790.10 | 357.41 | 107.71 | 269.80 | 13h 44m | 1d 3h 43m | 2d 9h 22m | 3d 17h 5m | max: 0.3313<br>last: 0.2448 |
379
+ | fully_async_policy | 16:16 | 294.77 | 21.26 | \ | 313.81 | 7h 58m<br>(1.72x) | 16h 21m<br>(1.70x) | 1d 0h 53m<br>(2.31x) | 1d 9h 26m<br>(2.66x) | max: 0.3302<br>last: 0.2333 |
380
+ | colocate sync | 64 | 365.28 | 150.72 | 70.26 | 133.41 | 10h 22m | 20h 45m | 1d 7h 6m | 1d 17h 32m | max: 0.3365<br>last: 0.2333 |
381
+ | fully_async_policy | 32:32 | 189.26 | 28.46 | \ | 156.98 | 4h 57m<br>(2.09x) | 10h 14m<br>(2.03x) | 16h 58m<br>(1.83x) | 21h 40m<br>(1.92x) | max: 0.3677<br>last: 0.3406 |
382
+ | colocate sync | 128 | 356.30 | 177.85 | 53.92 | 113.81 | 8h 36m | 17h 56m | 1d 5h 6m | 1d 16h 48m | max: 0.3573<br>last: 0.2958 |
383
+ | fully_async_policy | 64:64 | 150.63 | 33.14 | \ | 113.16 | 3h 13m<br>(2.67x) | 6h 46m<br>(2.65x) | 10h 53m<br>(2.67x) | 17h 22m<br>(2.35x) | max: 0.3521<br>last: 0.3094 |
384
+
385
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-colocate_async?nw=nwuserhouzg
386
+
387
+ ### 128-card 7B Asynchronous Mode Experiment
388
+
389
+ We used Qwen2.5-Math-7B to verify the effects of various modes supported by fully async.
390
+ We can see that the benefit brought by streaming is approximately 1.6x, and after combining staleness and
391
+ partial_rollout, the benefit reaches 2.35x.
392
+
393
+ | mode | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
394
+ |:-------------------------------------------------------------------------------------------------------:|:--------:|:--------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------------:|
395
+ | colocate sync | 356.30 | 177.85 | 53.92 | 113.81 | 8h 36m | 17h 56m | 1d 5h 6m | 1d 16h 48m | max: 0.3573<br>last: 0.2958 |
396
+ | `stream off policy pipeline`<br>(+fully async: trigger_parameter_sync_step= 4,<br>require_batches= 4) | 231.34 | 128.47 | \ | 98.77 | 4h 25m | 9h 41m | 15h 2m | 1d 1h 53m | max: 0.2844<br>last: 0.2604 |
397
+ | `async stream pipeline with stale samples`<br>(+staleness_threshold=0.5) | | | | | | | | | |
398
+ | `async stream pipeline with partial rollout`<br>(+partial_rollout=True) | 150.63 | 33.14 | \ | 113.16 | 3h 13m | 6h 46m | 10h 53m | 17h 22m | max: 0.3521<br>last: 0.3094 |
399
+
400
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-stream_stale_partial?nw=nwuserhouzg
401
+
402
+ ### 128-card Stale Ablation Experiment
403
+
404
+ Under the `async stream pipeline with partial rollout` mode, we verified the impact of staleness settings on training
405
+ efficiency.
406
+ We found that the larger the staleness, the more obvious the final gains.
407
+ We also noticed that the times for staleness values of 0.3 and 0.5 are quite close, because as the training steps
408
+ increase, the response length changes significantly, causing training instability.
409
+ Further analysis and optimization are needed for this issue.
410
+
411
+ | staleness_threshold | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
412
+ |:---------------------:|:--------:|:--------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:-----------------------------:|
413
+ | 0 | 231.34 | 128.47 | \ | 98.77 | 4h 25m | 9h 41m | 15h 2m | 1d 1h 53m | max: 0.2844<br>last: 0.2604 |
414
+ | 0.1 | 171.30 | 58.17 | \ | 109.12 | 3h 53m | 8h 37m | 14h 25m | 19h 59m | max: 0.3542<br>last: 0.2979 |
415
+ | 0.3 | 146.11 | 38.88 | \ | 103.22 | 3h 18m | 6h 49m | 11h 40m | 17h 20m | max: 0.3469<br>last: 0.2865 |
416
+ | 0.5 | 150.63 | 33.14 | \ | 113.16 | 3h 13m | 6h 46m | 10h 53m | 17h 22m | max: 0.3521<br>last: 0.3094 |
417
+
418
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-stream_stale_partial?nw=nwuserhouzg
419
+
420
+ ### 128-card 7B require_batches Ablation Experiment
421
+
422
+ In multiple tests, we found that the number of samples issued each time in streaming affects the response length during
423
+ training, which in turn affects training time. We verified the impact on results by modifying
424
+ `async_training.require_batches`.
425
+
426
+ | require_batches | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | acc/mean@1 |
427
+ |:-----------------:|:--------:|:-------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:-----------------------------:|
428
+ | 1 | 203.47 | 30.88 | \ | 181.08 | 3h 31m | 8h 29m | 17h 36m | max: 0.349<br>last: 0.326 |
429
+ | 2 | 158.72 | 26.32 | \ | 128.08 | 3h 35m | 7h 38m | 13h 57m | max: 0.351<br>last: 0.3406 |
430
+ | 4 | 124.64 | 25.62 | \ | 95.06 | 3h 13m | 6h 46m | 10h 53m | max: 0.3521<br>last: 0.3521 |
431
+
432
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-ablation_require_batches?nw=nwuserhouzg
433
+
434
+ ### 30B Model Mode Experiment
435
+
436
+ We achieved a 1.7x performance improvement with `async stream pipeline with staleness samples` strategy on the
437
+ Qwen3-30B-A3B-Base model compared to the colocate setup. It is worth noting that this is far from the upper limit of
438
+ performance gains achievable through asynchrony. Firstly, the comparative experiments used a maximum response length of
439
+ only 8k, which is much shorter than the 20k sequence length in previous experiments, resulting in a less pronounced
440
+ rollout tail effect. Secondly, we adopted a highly skewed resource allocation, with rollout using 96 GPUs and trainer
441
+ using 32 GPUs, which is not an optimal configuration. During the experiments, we observed that the current verl
442
+ implementation imposes certain constraints, such as requiring data to be evenly divisible by the number of GPUs, making
443
+ resource adjustment less flexible. Additionally, as asynchronous training and deployment accelerate, the performance gap
444
+ is gradually narrowing. Therefore, enabling more flexible resource allocation and dynamic resource adjustment in the
445
+ future will be our next focus.
446
+
447
+ * Machine: H20
448
+ * Model: Qwen3-30B-A3B-Base
449
+ * Rollout length: max_response_length : 8K tokens;
450
+ * Algorithm: GRPO
451
+ * Dataset: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
452
+ * Engine: vllm+Megatron
453
+ * rollout.n: 16
454
+ * ppo_mini_batch_size: 128
455
+ * test_freq: 20
456
+
457
+ * colocate sync:
458
+ * step:400
459
+ * train_batch_size: 512
460
+
461
+ * fully_async_policy
462
+ * total_rollout_steps: 512*400
463
+ * trigger_parameter_sync_step: 512/128 = 4
464
+ * staleness_threshold: 0.5
465
+ * partial_rollout: True
466
+
467
+ | Training Mode | Resource Allocation | Step | Gen | Old Log Prob | Ref | Update Actor | Total Time 100 Step | Total Time 200 Step | Total Time 300 Step | Total Time 400 Step | Acc/Mean@1 |
468
+ |--------------------|---------------------|--------|--------|--------------|-------|--------------|---------------------|---------------------|---------------------|---------------------|-----------------------------|
469
+ | Colocate Sync | 128 | 497.89 | 348.05 | 28.73 | 20.86 | 86.27 | 13h 36m | 1d 3h 48m | 1d 19h 4m | 2d 11h 39m | max: 0.3500<br>last: 0.3208 |
470
+ | Fully Async Policy | 96:32 | 282.75 | 22.06 | \ | 50.05 | 206.63 | 6h 45m (2.01x) | 14h 48m (1.88x) | 1d 0h 9m (1.78x) | 1d 10h 41m (1.72x) | max: 0.3813<br>last: 0.3448 |
471
+
472
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-30B?nw=nwuserhouzg | | |
473
+
474
+ ### checkpoint-engine Ablation Experiment
475
+ We tested the single-step parameter synchronization time of the checkpoint-engine on three models: Qwen2.5-Math-7B, Qwen3-30B-A3B, and Qwen3-235B-A22B, using default checkpoint-engine configurations. All experiments were performed on H20 machines, and the Megatron engine was used for training.
476
+ | model | trainer rank | rollout rank | checkpoint-engine | total sync time |
477
+ |:-----------------:|:--------:|:-------:|:--------------:|:--------------:|
478
+ | Qwen2.5-Math-7B | 4 | 4 | False | 0.12s |
479
+ | Qwen2.5-Math-7B | 4 | 4 | True | 0.02s |
480
+ | Qwen3-30B-A3B | 16 | 16 | False | 15.76s |
481
+ | Qwen3-30B-A3B | 16 | 16 | True | 4.38s |
482
+ | Qwen3-235B-A22B | 64 | 64 | False | 58.57s |
483
+ | Qwen3-235B-A22B | 64 | 64 | True | 23.70s |
484
+
485
+
486
+ ### use_trainer_do_validate Experiment
487
+ We tested the effect of setting `use_trainer_do_validate=True` on the training process. The results show that setting
488
+ this parameter to True can reduce the validation time overhead and trainer node idle time.
489
+ We used Qwen2.5-Math-7B to verify the benefits of `use_trainer_do_validate=True` on the training process, we achieved about 2x performance improvement on validation time, and the trainer node idle time is reduced by about 40%.
490
+
491
+ * Machine: H20
492
+ * Model: Qwen2.5-Math-7B
493
+ * Rollout length: max_response_length FSDP2: 10K tokens;
494
+ * Algorithm: DAPO
495
+ * Dataset: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
496
+ * Engine: vllm+FSDP2
497
+ * rollout.n: 16
498
+ * ppo_mini_batch_size: 32
499
+ * test_freq: 10
500
+
501
+ * fully_async_policy
502
+ * total_rollout_steps: 512*400
503
+ * require_batches: 4
504
+ * trigger_parameter_sync_step: 4
505
+ * staleness_threshold: 0.5
506
+ * partial_rollout: True
507
+
508
+ | training mode | resource allocation | step | gen | old_log_prob | update_actor | validate time | total time<br>50 step | acc/mean@2 |
509
+ |:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|
510
+ | colocate sync | 16 | 484.623 | 52.939 | 0 | 430.263 | 205.080 | 7h9m | 22.6 |
511
+ | fully_async_policy | 8:8 | 489.953 | 52.622 | 0 | 435.874 | 95.699 | 7h2m | 21.0 |
512
+
513
+
514
+ ## Multi-Turn Tool Calling
515
+
516
+ Referencing **recipe/retool** and **ToolAgentLoop**, we implemented **AsyncPartialToolAgentLoop**, a multi-turn
517
+ tool-calling loop that supports partial_rollout for **fully_async_policy**.
518
+
519
+ ### Core Design
520
+
521
+ `AsyncPartialToolAgentLoop` inherits from `ToolAgentLoop` and is adapted for the asynchronous training mode of
522
+ `fully_async_policy`. When `partial_rollout=True`, the Rollouter interrupts ongoing generation tasks before
523
+ synchronizing parameters with the Trainer. `AsyncPartialToolAgentLoop` is capable of:
524
+
525
+ 1. **Interrupting Tasks**: Responding to an interrupt signal to save the current state. Currently, interruptions occur
526
+ during the `GENERATING` process or after other states have completed.
527
+ 2. **Resuming Tasks**: Resuming execution from the saved state after parameter synchronization is complete, rather than
528
+ starting over.
529
+
530
+ ### How to Use
531
+
532
+ RL training with multi-turn tool calling in `fully_async_policy` is similar to `recipe/retool`. It is enabled by
533
+ specifying `multi_turn` configurations in the config file.
534
+
535
+ 1. **SFT Stage**: First, the model should undergo SFT to learn how to follow tool-calling format instructions.
536
+ 2. **Multi-turn Configuration**: In the `fully_async_policy` training configuration, set the following parameters:
537
+ ```yaml
538
+ actor_rollout_ref:
539
+ rollout:
540
+ multi_turn:
541
+ enable: True # AsyncPartialToolAgentLoop will be used by default in fully_async_policy mode
542
+ # Other multi_turn related configurations
543
+ ```
544
+ 3. **Async Parameters**: To improve efficiency, enable `partial_rollout` and `staleness_threshold` when using multi-turn
545
+ tool calling:
546
+ ```yaml
547
+ async_training:
548
+ partial_rollout: True
549
+ staleness_threshold: 0.5
550
+ # Other async parameters
551
+ ```
552
+ 4. **Example**: See `recipe/fully_async_policy/shell/dapo_7b_async_retool.sh`.
553
+
554
+ ### Experimental Results
555
+
556
+ To validate the performance of `fully_async_policy` on multi-turn tool-calling tasks, we compared it with the standard
557
+ `colocate` synchronous mode. Key parameter settings are as follows.
558
+
559
+ * **SFT Model**: Based on `Qwen2.5-7B-Instruct`, trained for 6 epochs on the `ReTool-SFT` dataset
560
+ * **RL Algorithm**: DAPO
561
+ * **Dataset**:
562
+ * Train: `DAPO-Math-17k`
563
+ * Test: `aime_2025`
564
+ * **Resource and Mode Comparison**:
565
+ * `colocate sync`: 32 H20 gpus
566
+ * `fully_async_policy`: 16 gpus for Trainer + 16 gpus for Rollouter
567
+ * **Key Configurations**:
568
+ 1. **Tool Calling Configuration**:
569
+ * `multi_turn.enable: True`
570
+ * `multi_turn.max_user_turns: 16`
571
+ * `multi_turn.max_assistant_turns: 16`
572
+ * `multi_turn.tool_config_path: recipe/retool/sandbox_fusion_tool_config.yaml`
573
+ 2. **`colocate sync` Configuration**:
574
+ * `ppo_mini_batch_size: 16`
575
+ * `train_batch_size: 64`
576
+ 3. **`fully_async_policy` Configuration**:
577
+ * `ppo_mini_batch_size: 16`
578
+ * `trigger_parameter_sync_step: 4`
579
+ * `require_batches: 1`
580
+ * `staleness_threshold: 1`
581
+ * `partial_rollout: True`
582
+
583
+ | training mode | Resource allocation | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | aime_2025<br>acc/mean@30 |
584
+ |:--------------------:|:---------------------:|:---------:|:---------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:-------------------------------:|
585
+ | colocate | 32 | 375.47 | 228.03 | 35.19 | 111.84 | 9h 46m | 22h 28m | start:0.1078<br>last:0.2056 |
586
+ | fully_async_policy | 16: 16 | 221.36 | 40.59 | \ | 179.58 | 6h 19m<br>(1.55x) | 14h 4m<br>(1.60x) | start:0.11<br>last:0.2044 |
587
+
588
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-multiturn-tool?nw=nwuserhouzg
589
+
590
+ ## Future Plans
591
+
592
+ * GRPO experiments
593
+ * Megatron adaptation
594
+ * SGLang integration
595
+ * Transfer queue integration
596
+ * Asynchronous parameter synchronization
597
+ * AReaL asynchronous algorithm implementation
598
+ * TPPO algorithm implementation
599
+ * Multi-turn and Tool support
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README_zh.md ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Recipe: Fully Async Policy Trainer
2
+
3
+ **Author:** `https://github.com/meituan-search`
4
+
5
+ Last updated: 12/15/2025.
6
+
7
+ 本文档介绍了完全异步PPO训练系统,该系统实现了 Trainer 和 Rollouter 的完全解耦,支持异步样本生成和训练。
8
+ 在该系统下,我们使用128卡训练qwen2.5-7B模型取得了2.35x-2.67x的性能提升,同时效果没有显著受到影响。
9
+
10
+ ## Introduction
11
+
12
+ ### Background
13
+
14
+ rollout和train分离架构相较于colocate的架构能够更加灵活地分配资源,设计更加灵活的训练逻辑,从而处理长尾等问题带来的GPU利用率低,训练效率低的问题。
15
+ one_step_off_policy通过分离架构的设计并进行rollout和train一轮异步的训练方法,缓解了rollout时间过长的问题,并在训练效率上取得了一些收益,
16
+ 但其强制使用一轮异步的数据,存在不够灵活等问题,而且并不能完全去除长尾对训练效率带来的的影响;在其他框架如areal、Magistral、streamrl、asyncflow上,
17
+ 已经基于分离架构实现了异步训练、流式训练,并取得了收益;我们借鉴其方法,在verl上进行了实现。fully_async_policy支持异步、流式、partial
18
+ rollout的训练, 通过合理设置资源分配情况、参数同步频率等参数,fully_async_policy能够显著提高训练效率。
19
+
20
+ > Magistral https://arxiv.org/abs/2506.10910
21
+ >
22
+ > AReaL: A Large-Scale Asynchronous Reinforcement Learning System for Language
23
+ > Reasoning https://arxiv.org/abs/2505.24298
24
+ >
25
+ > StreamRL: Scalable, Heterogeneous, and Elastic RL for LLMs with Disaggregated Stream
26
+ > Generation https://arxiv.org/abs/2504.15930
27
+ >
28
+ > AsyncFlow: An Asynchronous Streaming RL Framework for Efficient LLM Post-Training https://arxiv.org/abs/2507.01663
29
+ >
30
+
31
+ ### 核心贡献
32
+
33
+ * **资源隔离**:与使用hybrid_engine不同,Rollouter和Trainer使用分离的计算资源,需要分别指定所占用的资源。
34
+ * **生成与训练并行**:Trainer在训练的同时,Rollouter在生成新的样本。
35
+ * **多步异步**: 相比 one step off policy 支持0.x步到多步的异步设定,异步方案更加灵活。
36
+ * **nccl参数同步**:基于nccl通信原语,参考[checkpoint-engine](https://github.com/MoonshotAI/checkpoint-engine)实现Rollouter与Trainer间的高效参数同步。
37
+ * **Stream推理与训练**:Rollouter逐样本生成数据,同时数据传输以单个sample为最小传输单位。
38
+ * **异步训练与新鲜度控制**:通过设置参数async_training.staleness_threshold,支持使用旧参数生成的样本进行训练。
39
+ * **PartialRollout**: Rollouter推理过程支持partial rollout逻辑,通过参数同步时,添加`sleep()`和`resume()`
40
+ 逻辑,保存进行中的rollout的样本,并在下一次rollout中继续使用,减少参数同步等待进行中的任务结束时间。
41
+
42
+ 目前支持使用模式为 megatron/fsdp+vllm。vllm必须使用基于AgentLoop的server模式。
43
+
44
+ ## 设计
45
+
46
+ fully_async_policy的整体架构如下图所示,fully_async_policy主要由Rollouter、MessageQueue、Trainer、ParameterSynchronizer四部分组成。
47
+
48
+ ![fully_async_policy_structure](
49
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_structure.svg?raw=true)
50
+
51
+ 1. Rollouter逐样本生成序列,并将生成的sample放入MessageQueue中,生产的速度受新鲜度控制。
52
+ 2. MessageQueue用于暂存Rollouter生成的sample。
53
+ 3. Trainer逐样本从MessageQueue中获取,获取到`require_batches*ppo_mini_batch_size`
54
+ 数量的样本后,就会进行训练,训练async_training.trigger_parameter_sync_step轮后,触发与Rollouter的一次参数同步。
55
+ 4. ParameterSynchronizer 实现了Nccl的同步参数同步能力。
56
+
57
+ 当前方案对比base的收益来源,在于colocate情况下,rollout使用更多的资源无法解决长尾样本带来的空闲,
58
+ 当我们进行资源隔离后,rollout的时间和train的时间都可能相较于之前更长(因为使用的资源变少了),
59
+ 但是相互之间的耗时overlap,端到端的耗时反而有所缩减。
60
+
61
+ ![fully_async_policy_revenue](
62
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_revenue.svg?raw=true)
63
+
64
+ ## 使用方式
65
+
66
+ ### 参数说明
67
+
68
+ | super params | implication |
69
+ |------------------------------------------------------|-----------------------------------------------------------------|
70
+ | `trainer.nnodes` | Trainer的node数量 |
71
+ | `trainer.n_gpus_per_node` | Trainer每个node上gpu的数量 |
72
+ | `rollout.nnodes` | Rollouter的node数量 |
73
+ | `rollout.n_gpus_per_node` | Rollouter每个node上gpu的数量 |
74
+ | `data.train_batch_size` | 在fully async策略中,该值不生效(默认设置为0) |
75
+ | `data.gen_batch_size` | 在fully async策略中,使用流式的样本生产逻辑(默认设置为1) |
76
+ | `rollout.total_rollout_steps` | 总的rollout的sample数量 |
77
+ | `rollout.test_freq` | Rollouter每更新多少次参数,进行一次validation |
78
+ | `actor_rollout_ref.actor.ppo_mini_batch_size` | The ppo_mini_batch_size is a global num across all workers/gpus |
79
+ | `async_training.require_batches` | FullyAsyncTrainer一次性获取的ppo_mini_batch_size的数量 |
80
+ | `async_training.trigger_parameter_sync_step` | 表示FullyAsyncTrainer进行多少次本地更新后,进行一次参数同步 |
81
+ | `async_training.staleness_threshold` | 新鲜度控制 |
82
+ | `async_training.partial_rollout` | 是否进行partial_rollout |
83
+ | `async_training.use_rollout_log_probs` | 使用rollout产生的log_probs |
84
+ | `async_training.compute_prox_log_prob`(experimental) | 是否在train阶段,使用train模型的参数计算token的 log_prob |
85
+ | `async_training.checkpoint_engine.enable`| 是否开启checkpoint_engine模式的加速,默认值True |
86
+ | `async_training.checkpoint_engine.overlap_broadcast_and_consume` | 启动checkpoint_engine时,是否在参数同步时在broadcast和加载之间使用流水,默认值False|
87
+ | `async_training.checkpoint_engine.device_buffer_size_M` | 启动checkpoint_engine时,组装的bucket的大小(MB),默认为4096 |
88
+ | `async_training.use_trainer_do_validate` | 是否使用Trainer的do_validate方法进行validation,默认值False |
89
+
90
+ **进一步的解释:**
91
+
92
+ * `rollout.total_rollout_steps`
93
+
94
+ 与 colocate 相比,数量可以通过 train_batch_size 与 step 相乘对齐:
95
+ `rollout.total_rollout_steps = data.train_batch_size * step`。
96
+
97
+ * `async_training.trigger_parameter_sync_step`
98
+
99
+ 在fully async策略中,表示Trainer进行多少次本地更新后(也就是获取多少次`require_batches * ppo_mini_batch_size`数量样本),
100
+ 与Rollouter之间进行一次参数同步。
101
+ 每两次Rollouter和Trainer参数同步之间,Trainer将会处理`trigger_parameter_sync_step* require_batches\
102
+ ppo_mini_batch_size`份sample。
103
+ 如果为了与colocate在公平的情况下对比速度,trigger_parameter_sync_step应该设置为 `data.train_batch_size / (
104
+ require_batches * ppo_mini_batch_size)`。
105
+
106
+ * `async_training.staleness_threshold`
107
+
108
+ 在fully async策略中,表示最大允许使用的staleness样本的比例。
109
+
110
+ * staleness_threshold=0,表示同步训练。
111
+ Rollouter两次参数更新之间将会生成固定数量的样本,样本数为:
112
+ $$rollout\_num = (trigger\_parameter\_sync\_step*require\_batches*ppo\_mini\_batch\_size)$$
113
+ * staleness_threshold>0,表示异步训练, 可以设置为小数,支持更灵活的异步调用。
114
+ Rollouter两次参数更新之间将会最多生成的样本数为:
115
+ $$rollout\_num = (1+staleness\_threshold)*(trigger\_parameter\_sync\_step*require\_batches*ppo\_mini\_batch\_size) - num\_staleness\_sample $$
116
+
117
+ num_staleness_sample 表示上一次rollout多生成的陈旧样本数。
118
+
119
+ 由于是流式系统,rollout持续生成,trainer持续消费。如果rollouter较慢,trainer会更早触发参数同步,rollouter并不会实际生产rollout_num个样本。
120
+ 当rollout 足够快时,staleness_threshold设置为1,基本上等价于one_step_off policy。
121
+ 为了避免过期样本太多影响训练精度,建议该值设置小于1。
122
+
123
+ * `async_training.partial_rollout`
124
+
125
+ partial_rollout只会在staleness_threshold>0时才实际上起作用。
126
+
127
+ * `async_training.use_rollout_log_probs`
128
+
129
+ 在强化学习算法中,log_probs与参数版本,token都存在隐性的相关性。由于PPO/GRPO/DAPO等算法的设定,我们在计算重要性采样时,
130
+ 即 old_log_prob必须使用rollout参数及token所对应log_probs,才能保证算法的正确性。在fully
131
+ async策略中,我们默认old_log_prob是有rollout所计算的,而不是由trainer所计算。
132
+
133
+ * `async_training.require_batches`
134
+
135
+ 在流式训练中,require_batches 应该设置为1,表示生产够ppo_mini_batch_size样本后,就进行训练。
136
+ 在实际测试中,我们发现,如果单次下发的样本较少,由于数据分发的顺序,会导致训练不稳定,response 长度变长。
137
+ 在这里,我们额外提供 require_batches 进行流式分发,单次参与训练的样本数量控制。
138
+
139
+ * `async_training.compute_prox_log_prob` (experimental)
140
+
141
+ 我们在训练过程中,观测到随着训练的进行,训练后期指标和response长度可能会出现不稳定的情况,
142
+ 这里我们可以使用 [Rollout Importance Sampling](https://verl.readthedocs.io/en/latest/advance/rollout_is.html) 的技术进行
143
+ 重要性采样,缓解这一问题。为了使用 `Rollout Importance Sampling` 我们需要使用训练引擎使用当前的参数版本计算old_log_prob,此开关需要打开。
144
+ 此外,在 mode d (async stream pipeline with partial rollout) 的情况下开启 `compute_prox_log_prob` 以及
145
+ `Rollout Importance Sampling` 后,我们的实现已近似Areal的 `Decoupled PPO`。
146
+
147
+ * `async_training.checkpoint_engine.enable`
148
+
149
+ 开启checkpoint engine后,相较于原始的逐tensor的参数同步方式,同步时间开销普遍可以降低60%以上。但是组装bucket会带来额外的临时显存开销。
150
+
151
+ * `async_training.checkpoint_engine.overlap_broadcast_and_consume`
152
+
153
+ 开启参数broadcast和load_weights之间的流水后,会进一步额外申请更多显存。由于目前分析参数同步的主要耗时并非来自broadcast和load_weights阶段,而是在参数生成阶段(由megatron或FSDP),因此该开关默认关闭。
154
+
155
+ * `async_training.checkpoint_engine.device_buffer_size_M`
156
+
157
+ 控制开启checkpoint engine后,用于同步的显存buffer大小。实际的`bucket_size` = `max(device_buffer_size_M, 最大参数tensor size)`
158
+ * 在开启`overlap_broadcast_and_consume`时,trainer节点的临时额外显存开销为 `3 * bucket_size`, rollout节点的临时额外显存开销为`2 * bucket_size`。
159
+ * 在关闭`overlap_broadcast_and_consume`时,trainer节点的临时额外显存开销为 `2 * bucket_size`, rollout节点的临时额外显存开销为`1 * bucket_size`。
160
+
161
+ * `async_training.use_trainer_do_validate`
162
+
163
+ 控制是否使用trainer的`do_validate`方法进行validation。
164
+ 如果设置为True,trainer会在每次参数更新后,调用`do_validate`方法进行validation。
165
+ 如果设置为False,trainer不会调用`do_validate`方法。
166
+
167
+ ### 模式支持
168
+
169
+ 1. on policy pipeline:
170
+ 1. **trigger_parameter_sync_step=1,staleness_threshold=0**
171
+ 2. Rollouter一次生产`require_batches*ppo_mini_batch_size`
172
+ 的samples,Trainer获取这些samples后进行训练,训练完后Trainer和Rollouter之间进行一次参数同步;
173
+ 3. 在rollout阶段,如果存在长尾的样本,但是rollout样本数较少时,较短的样本无法填充到空闲的资源中,会造成一定的资源浪费。
174
+ 4. 如图a所示;
175
+
176
+ 2. stream off policy pipeline:
177
+ 1. **trigger_parameter_sync_step>1,staleness_threshold=0**
178
+ 2. 将会进行同步的流式训练,Rollouter一次生产`require_batches*ppo_mini_batch_size*trigger_parameter_sync_step`
179
+ 的samples,Trainer每获取`require_batches*ppo_mini_batch_size`
180
+ 就进行一次本地训练,训练trigger_parameter_sync_step次后,Trainer和Rollouter之间进行一次参数同步;
181
+ 3. 相较于a,由于一次生成的样本更多,资源的空闲会更低。
182
+ 4. 在一次step训练中,会存在两次资源闲置的时间,分别是在第一次获取样本时,train等待`require_batches*ppo_mini_batch_size`
183
+ 个样本生产,以及最后一次参数更新时,rollout等待训练完成。
184
+ 5. 如图b所示;
185
+
186
+ 3. async stream pipeline with staleness samples:
187
+ 1. **trigger_parameter_sync_step>=1,staleness_threshold>0,partial_rollout=Flase**
188
+ 2. Rollouter在每次参数更新后将计划最多生产rollout_num个样本(实际根据rollout速度,生成的样本可能会少与这个值)。
189
+ 3. 如果rollout过程比较快,Rollouter将会在参数同步前额外生成一部分样本num_stale_samples,用于参数同步后立即给Trainer使用。
190
+ 触发参数同步时,如果Rollouter有正在生产的任务,将会等待任务完成,同时不会添加新的任务;
191
+ 4. 相较于b,除第一次step训练外,后续的训练都不会有wait first batch rollout finish的时间,但是会有wait active task
192
+ finish的时间。
193
+ 5. 如图c所示;
194
+
195
+ 4. async stream pipeline with partial rollout:
196
+ 1. **trigger_parameter_sync_step>=1,staleness_threshold>0,partial_rollout=True**
197
+ 2. 相较于c,触发参数同步时,Rollouter如果有正在生产的sample,会打断rollout过程并进行参数同步,被中断的sample会在参数同步后继续生成。减少了wait
198
+ active task finish的时间。
199
+ 3. 如图d所示;
200
+
201
+ ![fully_async_policy_mode](
202
+ https://github.com/ArronHZG/verl-community/blob/main/docs/fully_async_policy_mode.svg?raw=true)
203
+
204
+ ### 关键指标
205
+
206
+ | metrics | implication |
207
+ |------------------------------------------------|-----------------------------------------------------------|
208
+ | `trainer/idle_ratio` | Trainer闲置率 |
209
+ | `rollouter/idle_ratio` | Rollouter闲置率 |
210
+ | `fully_async/count/stale_samples_processed` | 训练使用的旧sample总数 |
211
+ | `fully_async/count/stale_trajectory_processed` | 训练使用的旧trajectory总数(一个sample会生产rollout.n条trajectory) |
212
+ | `fully_async/partial/total_partial_num` | 两次trigger_parameter_sync_step之间Trainer处理的partial样本数 |
213
+ | `fully_async/partial/partial_ratio` | 两次trigger_parameter_sync_step之间Trainer处理的partial样本的比例 |
214
+ | `fully_async/partial/max_partial_span` | 两次trigger_parameter_sync_step之间Trainer处理的partial样本的最大参数跨度 |
215
+
216
+ ### 调参建议
217
+
218
+ * 资源分配与调整:
219
+ * 合理的资源分配是获得好的训练效率的前提。理想的资源分配情况应该是使得Rollout的时间和Train的时间接近,从而使得整个训练过程流水气泡最小,
220
+ 避免资源闲置,同时Trainer不会使用旧样本。在真实训练场景下,可以根据实际训练过程中rollout和train的空闲时间调整资源分配,
221
+ 可从rollouter/idle_ratio和trainer/idle_ratio获得,如果rollouter/idle_ratio较高trainer/idle_ratio较低,
222
+ 应该增多Trainer的资源减少Rollouter的资源,反之亦然。
223
+
224
+ * 关键参数:
225
+ * staleness_threshold: 设置太大会导致较多的旧样本使用,影响模型效果,建议设置小于1。
226
+ * require_batches:越接近1,越接近纯流式过程,训练过程中bubble越小,能够在速度上获得更快的加速效果,但会对样本的处理顺序产生影响;
227
+ * trigger_parameter_sync_step: 设置的越小越接近on policy,但会导致频繁的参数同步,长尾样本浪费的资源无法被短样本填充,资源利用率低。
228
+ 设置的越大有更高的计算效率,但是精度上会受到off policy的影响。
229
+ * rollout.test_freq: 会占用Rollouter资源,不建议设置太小。
230
+
231
+ * 模式选择:通过调整不同的参数,Fully Async架构支持不同程度上的优化加速,适用于不同场景的任务。
232
+ * 对于小规模任务,需要保证训练的稳定性和 on-policy 性,对速度要求不高的场景,可以尝试使用on policy pipeline的模式(模式1)。
233
+ * 对于需要提高训练吞吐量,但对 staleness 敏感的场景,可以尝试使用 stream off policy pipeline 的模式。即通过
234
+ 设置trigger_parameter_sync_step>1 ,提高 训练效率,但仍保持同步机制 (staleness_threshold=0 )(模式2)。
235
+ * 对于大规模任务,对训练速度有较高要求,且可以容忍一定 off-policy 程度、staleness的场景,可以设置staleness_threshold>
236
+ 0、partial_rollout=True提高训练效率,使用 async stream pipeline 模式(模式 3 或 4)。
237
+
238
+ ### 快速开始
239
+
240
+ ```shell
241
+ rollout_mode="async"
242
+ rollout_name="vllm" # sglang or vllm
243
+ if [ "$rollout_mode" = "async" ]; then
244
+ export VLLM_USE_V1=1
245
+ return_raw_chat="True"
246
+ fi
247
+
248
+ train_prompt_bsz=0
249
+ gen_prompt_bsz=1
250
+ n_resp_per_prompt=16
251
+ train_prompt_mini_bsz=32
252
+ total_rollout_steps=$(((512*400)))
253
+ test_freq=10
254
+ staleness_threshold=0
255
+ trigger_parameter_sync_step=16
256
+ partial_rollout=False
257
+
258
+
259
+ python -m recipe.fully_async_policy.fully_async_main \
260
+ train_batch_size=${train_prompt_bsz} \
261
+ data.gen_batch_size=${gen_prompt_bsz} \
262
+ data.return_raw_chat=${return_raw_chat} \
263
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
264
+ actor_rollout_ref.actor.strategy=fsdp2 \
265
+ critic.strategy=fsdp2 \
266
+ actor_rollout_ref.hybrid_engine=False \
267
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
268
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
269
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
270
+ actor_rollout_ref.rollout.name=${rollout_name} \
271
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
272
+ actor_rollout_ref.rollout.calculate_log_probs=True \
273
+ trainer.nnodes="${NNODES_TRAIN}" \
274
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
275
+ rollout.nnodes="${NNODES_ROLLOUT}" \
276
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
277
+ rollout.total_rollout_steps="${total_rollout_steps}" \
278
+ rollout.test_freq="${test_freq}" \
279
+ async_training.staleness_threshold="${staleness_threshold}" \
280
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
281
+ async_training.partial_rollout="${partial_rollout}"
282
+ ```
283
+
284
+ ## 实验
285
+
286
+ ### 在7B模型上进行异步训练
287
+
288
+ 我们使用 Qwen2.5-Math-7B 验证 fully async 策略在长候选下,多种资源下的收益情况。
289
+ 使用`async stream pipeline with staleness samples` 策略,我们在32卡,64卡,128卡都取得2x左右的性能提升,同时没有显著影响实验效果。
290
+
291
+ * 机器:H20
292
+ * 模型:Qwen2.5-Math-7B
293
+ * rollout长度:max_response_length FSDP2: 28K tokens;
294
+ * 算法:DAPO
295
+ * 数据集: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
296
+ * engine: vllm+FSDP2
297
+ * rollout.n: 16
298
+ * ppo_mini_batch_size: 32
299
+ * test_freq: 20
300
+
301
+ * colocate sync:
302
+ * step: 400
303
+ * train_batch_size: 512
304
+
305
+ * fully_async_policy
306
+ * total_rollout_steps: 512*400
307
+ * require_batches: 4
308
+ * trigger_parameter_sync_step: 4
309
+ * staleness_threshold: 0.5
310
+ * partial_rollout: True
311
+
312
+ | training mode | resource allocation | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
313
+ |:--------------------:|:---------------------:|:--------:|:--------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:-------------------------------:|
314
+ | colocate sync | 32 | 790.10 | 357.41 | 107.71 | 269.80 | 13h 44m | 1d 3h 43m | 2d 9h 22m | 3d 17h 5m | max: 0.3313<br>last: 0.2448 |
315
+ | fully_async_policy | 16:16 | 294.77 | 21.26 | \ | 313.81 | 7h 58m<br>(1.72x) | 16h 21m<br>(1.70x) | 1d 0h 53m<br>(2.31x) | 1d 9h 26m<br>(2.66x) | max: 0.3302<br>last: 0.2333 |
316
+ | colocate sync | 64 | 365.28 | 150.72 | 70.26 | 133.41 | 10h 22m | 20h 45m | 1d 7h 6m | 1d 17h 32m | max: 0.3365<br>last: 0.2333 |
317
+ | fully_async_policy | 32:32 | 189.26 | 28.46 | \ | 156.98 | 4h 57m<br>(2.09x) | 10h 14m<br>(2.03x) | 16h 58m<br>(1.83x) | 21h 40m<br>(1.92x) | max: 0.3677<br>last: 0.3406 |
318
+ | colocate sync | 128 | 356.30 | 177.85 | 53.92 | 113.81 | 8h 36m | 17h 56m | 1d 5h 6m | 1d 16h 48m | max: 0.3573<br>last: 0.2958 |
319
+ | fully_async_policy | 64:64 | 150.63 | 33.14 | \ | 113.16 | 3h 13m<br>(2.67x) | 6h 46m<br>(2.65x) | 10h 53m<br>(2.67x) | 17h 22m<br>(2.35x) | max: 0.3521<br>last: 0.3094 |
320
+
321
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-colocate_async?nw=nwuserhouzg
322
+
323
+ ### 128卡 7B 异步模式实验
324
+
325
+ 我们使用 Qwen2.5-Math-7B 验证 fully async 所支持的各个模式的效果。
326
+ 我们可以看到 stream 带来的收益大约1.6x,叠加 staleness 和 partial_rollout 后,收益为2.35x。
327
+
328
+ | mode | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
329
+ |:-------------------------------------------------------------------------------------------------------:|:--------:|:--------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------------:|
330
+ | colocate sync | 356.30 | 177.85 | 53.92 | 113.81 | 8h 36m | 17h 56m | 1d 5h 6m | 1d 16h 48m | max: 0.3573<br>last: 0.2958 |
331
+ | `stream off policy pipeline`<br>(+fully async: trigger_parameter_sync_step= 4,<br>require_batches= 4) | 231.34 | 128.47 | \ | 98.77 | 4h 25m | 9h 41m | 15h 2m | 1d 1h 53m | max: 0.2844<br>last: 0.2604 |
332
+ | `async stream pipeline with staleness samples`<br>(+staleness_threshold=0.5) | | | | | | | | | |
333
+ | `async stream pipeline with partial rollout`<br>(+partial_rollout=True) | 150.63 | 33.14 | \ | 113.16 | 3h 13m | 6h 46m | 10h 53m | 17h 22m | max: 0.3521<br>last: 0.3094 |
334
+
335
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-stream_stale_partial?nw=nwuserhouzg
336
+
337
+ ### 128卡 stale 消融实验
338
+
339
+ 在 `async stream pipeline with partial rollout` 模式下,我们验证 staleness 的设置对于训练效率的影响。
340
+ 我们可以发现,staleness 越大,最终取得的收益越明显。
341
+ 同时我们也注意到 staleness 取 0.3 和 0.5 的时间比较接近,原因是随着训练步数的增量,response 长度变化较大,训练出现了不稳定的问题。
342
+ 后续还需要针对该问题进行进一步的分析和优化。
343
+
344
+ | staleness_threshold | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | total time<br>400 step | acc/mean@1 |
345
+ |:---------------------:|:--------:|:--------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:------------------------:|:-----------------------------:|
346
+ | 0 | 231.34 | 128.47 | \ | 98.77 | 4h 25m | 9h 41m | 15h 2m | 1d 1h 53m | max: 0.2844<br>last: 0.2604 |
347
+ | 0.1 | 171.30 | 58.17 | \ | 109.12 | 3h 53m | 8h 37m | 14h 25m | 19h 59m | max: 0.3542<br>last: 0.2979 |
348
+ | 0.3 | 146.11 | 38.88 | \ | 103.22 | 3h 18m | 6h 49m | 11h 40m | 17h 20m | max: 0.3469<br>last: 0.2865 |
349
+ | 0.5 | 150.63 | 33.14 | \ | 113.16 | 3h 13m | 6h 46m | 10h 53m | 17h 22m | max: 0.3521<br>last: 0.3094 |
350
+
351
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-ablation_stale?nw=nwuserhouzg
352
+
353
+ ### 128卡 7B require_batches 消融实验
354
+
355
+ 在多次测试下,我们发现流式每次下发样本的数量会影响训练的response长度,进而影响训练时长,我们通过修改
356
+ `async_training.require_batches` 验证对与结果的影响。
357
+
358
+ | require_batches | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | total time<br>300 step | acc/mean@1 |
359
+ |:-----------------:|:--------:|:-------:|:--------------:|:--------------:|:------------------------:|:------------------------:|:------------------------:|:-----------------------------:|
360
+ | 1 | 203.47 | 30.88 | \ | 181.08 | 3h 31m | 8h 29m | 17h 36m | max: 0.349<br>last: 0.326 |
361
+ | 2 | 158.72 | 26.32 | \ | 128.08 | 3h 35m | 7h 38m | 13h 57m | max: 0.351<br>last: 0.3406 |
362
+ | 4 | 124.64 | 25.62 | \ | 95.06 | 3h 13m | 6h 46m | 10h 53m | max: 0.3521<br>last: 0.3521 |
363
+
364
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-ablation_require_batches?nw=nwuserhouzg
365
+
366
+ ### 30B模型模式实验
367
+
368
+ 我们在 Qwen3-30B-A3B-Base 模型上通过`async stream pipeline with staleness samples` 策略,相比于 colocate 方案取得了 1.7
369
+ 倍的性能提升。值得说明的是,这距离异步方式所能带来的性能提升上限还有很大空间。首先,对比实验中使用的最大响应长度仅为
370
+ 8k,这远低于此前实验的 20k 序列长度,因此 rollout 的长尾效应并不明显。其次,我们采用了极为倾斜的资源分配方案,rollout 使用了
371
+ 96 张 GPU,而 trainer 仅使用了 32 张 GPU,这并不是最优的配置。在实验过程中,我们观察到当前的 verl 实现存在一些限制,比如要求数据必须能被
372
+ GPU 数量整除,这使得资源调整的灵活性受到影响。此外,随着异步训练和部署的加速,性能差距也在逐渐缩小。因此,未来我们将重点关注如何实现更灵活的资源分配和动态调整资源。
373
+
374
+ * 机器:H20
375
+ * 模型:Qwen3-30B-A3B-Base
376
+ * rollout长度:max_response_length : 8K tokens;
377
+ * 算法: GRPO
378
+ * 数据集: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
379
+ * Engine: vllm+Megatron
380
+ * rollout.n: 16
381
+ * ppo_mini_batch_size: 128
382
+ * test_freq: 20
383
+
384
+ * colocate sync:
385
+ * step:400
386
+ * train_batch_size: 512
387
+
388
+ * fully_async_policy
389
+ * total_rollout_steps: 512*400
390
+ * trigger_parameter_sync_step: 512/128 = 4
391
+ * staleness_threshold: 0.5
392
+ * partial_rollout: True
393
+
394
+ | Training Mode | Resource Allocation | Step | Gen | Old Log Prob | Ref | Update Actor | Total Time 100 Step | Total Time 200 Step | Total Time 300 Step | Total Time 400 Step | Acc/Mean@1 |
395
+ |----------------------|--------------------|---------|--------|--------------|--------|--------------|---------------------|---------------------|---------------------|---------------------|-----------------------------|
396
+ | Colocate Sync | 128 | 497.89 | 348.05 | 28.73 | 20.86 | 86.27 | 13h 36m | 1d 3h 48m | 1d 19h 4m | 2d 11h 39m | max: 0.3500<br>last: 0.3208 |
397
+ | Fully Async Policy | 96:32 | 282.75 | 22.06 | \ | 50.05 | 206.63 | 6h 45m (2.01x) | 14h 48m (1.88x) | 1d 0h 9m (1.78x) | 1d 10h 41m (1.72x) | max: 0.3813<br>last: 0.3448 |
398
+
399
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-30B?nw=nwuserhouzg
400
+
401
+ ### checkpoint-engine参数同步消融实验
402
+ 我们在Qwen2.5-Math-7B,Qwen3-30B-A3B和Qwen3-235B-A22B三个模型上测试了checkpoint-engine参数同步的单步参数同步耗时,使用的参数均为默认参数配置。实验均在H20机器上完成,并使用megatron训练引擎。
403
+ | model | trainer rank | rollout rank | checkpoint-engine | total sync time |
404
+ |:-----------------:|:--------:|:-------:|:--------------:|:--------------:|
405
+ | Qwen2.5-Math-7B | 4 | 4 | False | 0.12s |
406
+ | Qwen2.5-Math-7B | 4 | 4 | True | 0.02s |
407
+ | Qwen3-30B-A3B | 16 | 16 | False | 15.76s |
408
+ | Qwen3-30B-A3B | 16 | 16 | True | 4.38s |
409
+ | Qwen3-235B-A22B | 64 | 64 | False | 58.57s |
410
+ | Qwen3-235B-A22B | 64 | 64 | True | 23.70s |
411
+
412
+ ### use_trainer_do_validate 实验测试
413
+ 我们在Qwen2.5-Math-7B模型上测试了`use_trainer_do_validate`参数的影响。这个结果展示使用`use_trainer_do_validate=True`可以减少验证时间开销,并且训练器节点的空闲时间也减少了。
414
+
415
+ * Machine: H20
416
+ * Model: Qwen2.5-Math-7B
417
+ * Rollout length: max_response_length FSDP2: 10K tokens;
418
+ * Algorithm: DAPO
419
+ * Dataset: TRAIN_FILE: dapo-math-17k.parquet TEST_FILE: aime-2024.parquet
420
+ * Engine: vllm+FSDP2
421
+ * rollout.n: 16
422
+ * ppo_mini_batch_size: 32
423
+ * test_freq: 10
424
+
425
+ * fully_async_policy
426
+ * total_rollout_steps: 512*400
427
+ * require_batches: 4
428
+ * trigger_parameter_sync_step: 4
429
+ * staleness_threshold: 0.5
430
+ * partial_rollout: True
431
+
432
+ | training mode | resource allocation | step | gen | old_log_prob | update_actor | validate time | total time<br>50 step | acc/mean@2 |
433
+ |:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|:---------------:|
434
+ | colocate sync | 16 | 484.623 | 52.939 | 0 | 430.263 | 205.080 | 7h9m | 22.6 |
435
+ | fully_async_policy | 8:8 | 489.953 | 52.622 | 0 | 435.874 | 95.699 | 7h2m | 21.0 |
436
+
437
+
438
+ ## 多轮工具调用
439
+
440
+ 参考 **recipe/retool** 和 **ToolAgentLoop**,我们为 **fully_async_policy** 实现了支持partial rollout的多轮工具调用循环 *
441
+ *AsyncPartialToolAgentLoop**。
442
+
443
+ ### 核心设计
444
+
445
+ `AsyncPartialToolAgentLoop` 继承自 `ToolAgentLoop`,其核心是适配了 `fully_async_policy` 的异步训练模式。当
446
+ `partial_rollout=True` 时,Rollouter 在与 Trainer 同步参数前会中断正在进行的生成任务。`AsyncPartialToolAgentLoop` 能够:
447
+
448
+ 1. **中断任务**: 响应中断信号,保存当前的生成状态。目前,中断会发生在GENERATING过程中,或其他状态结束后;
449
+ 2. **恢复任务**: 在参数同步完成后,从保存的状态恢复,继续执行,而不是从头开始。
450
+
451
+ ### 使用方法
452
+
453
+ `fully_async_policy`多轮与工具调用的RL训练与 `recipe/retool` 类似,通过在配置文件中指定 `multi_turn` 相关配置来启用。
454
+
455
+ 1. **SFT 阶段**: 首先,需要对模型进行 SFT训练,使其具备遵循工具调用格式指令的能力。
456
+ 2. **配置启用**: 在 `fully_async_policy` 的训练配置中,设置以下参数:
457
+ ```yaml
458
+ actor_rollout_ref:
459
+ rollout:
460
+ multi_turn:
461
+ enable: True # 在fully_async_policy模式下将默认使用AsyncPartialToolAgentLoop
462
+ # 其他 multi_turn 相关配置
463
+ ```
464
+ 3. **配置async参数**: 为提高效率,在启用多轮工具调用时,同时开启 `partial_rollout`和`staleness_threshold`:
465
+ ```yaml
466
+ async_training:
467
+ partial_rollout: True
468
+ staleness_threshold: 0.5
469
+ # 其他async参数
470
+ ```
471
+ 4. **example**: 参考`recipe/fully_async_policy/shell/dapo_7b_async_retool.sh`
472
+
473
+ ### 实验结果
474
+
475
+ 为验证 `fully_async_policy` 在多轮工具调用任务中的性能,我们将其与标准 `colocate` 同步模式进行了对比。实验具体设置如下。
476
+
477
+ * **SFT模型**: 实验基于 `Qwen2.5-7B-Instruct` 模型,使用`ReTool-SFT`数据集训练6个epoch;
478
+ * **RL算法**: DAPO
479
+ * **数据集**:
480
+ * 训练集: `DAPO-Math-17k`
481
+ * 测试集: `aime_2025`
482
+ * **资源与模式对比**:
483
+ * `colocate sync`: 32卡 H20
484
+ * `fully_async_policy`: 16卡 Trainer + 16卡 Rollouter
485
+ * **关键配置**:
486
+ 1. **工具调用配置**:
487
+ * `multi_turn.enable: True`
488
+ * `multi_turn.max_user_turns: 16`
489
+ * `multi_turn.max_assistant_turns: 16`
490
+ * `multi_turn.tool_config_path: recipe/retool/sandbox_fusion_tool_config.yaml`
491
+ 2. **`colocate sync`配置**:
492
+ * `ppo_mini_batch_size: 16`
493
+ * `train_batch_size: 64`
494
+ 3. **`fully_async_policy`配置**:
495
+ * `ppo_mini_batch_size: 16`
496
+ * `trigger_parameter_sync_step: 4`
497
+ * `require_batches: 1`
498
+ * `staleness_threshold: 1`
499
+ * `partial_rollout: True`
500
+
501
+ | training mode | Resource allocation | step | gen | old_log_prob | update_actor | total time<br>100 step | total time<br>200 step | aime_2025<br>acc/mean@30 |
502
+ |:------------------: |:-------------------: |:-------: |:-------: |:------------: |:------------: |:----------------------: |:----------------------: |:---------------------------: |
503
+ | colocate | 32 | 375.47 | 228.03 | 35.19 | 111.84 | 9h 46m | 22h 28m | start:0.1078<br>last:0.2056 |
504
+ | fully_async_policy | 16: 16 | 221.36 | 40.59 | \ | 179.58 | 6h 19m<br>(1.55x) | 14h 4m<br>(1.60x) | start:0.11<br>last:0.2044 |
505
+
506
+ > source data: https://wandb.ai/hou-zg-meituan/fully-async-policy-multiturn-tool?nw=nwuserhouzg
507
+
508
+ ## 后续计划
509
+
510
+ * GRPO实验
511
+ * megatron 适配
512
+ * sglang 集成
513
+ * transfer queue 集成
514
+ * 异步参数同步
515
+ * Areal异步算法实现
516
+ * TPPO算法实现
517
+ * 多轮及Tool的支持
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .agent_loop import FullyAsyncAgentLoopManager
16
+ from .partial_single_turn_agent_loop import PartialSingleTurnAgentLoop
17
+ from .partial_tool_agent_loop import AsyncPartialToolAgentLoop
18
+
19
+ _ = [PartialSingleTurnAgentLoop, AsyncPartialToolAgentLoop]
20
+ __all__ = [FullyAsyncAgentLoopManager]
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/agent_loop.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import logging
16
+ import os
17
+ from typing import Any, Optional, Sequence
18
+
19
+ import hydra
20
+ import numpy as np
21
+ import ray
22
+ from omegaconf import DictConfig
23
+
24
+ from verl.experimental.agent_loop.agent_loop import (
25
+ AgentLoopManager,
26
+ AgentLoopOutput,
27
+ AgentLoopWorker,
28
+ AsyncLLMServerManager,
29
+ DictConfigWrap,
30
+ _agent_loop_registry,
31
+ get_trajectory_info,
32
+ )
33
+ from verl.experimental.agent_loop.prometheus_utils import update_prometheus_config
34
+ from verl.protocol import DataProto
35
+ from verl.single_controller.ray import RayResourcePool, RayWorkerGroup
36
+ from verl.utils.rollout_trace import (
37
+ rollout_trace_attr,
38
+ rollout_trace_op,
39
+ )
40
+
41
+ logger = logging.getLogger(__file__)
42
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
43
+
44
+
45
+ class FullyAsyncLLMServerManager(AsyncLLMServerManager):
46
+ @rollout_trace_op
47
+ async def generate_for_partial(
48
+ self,
49
+ request_id,
50
+ *,
51
+ prompt_ids: list[int],
52
+ sampling_params: dict[str, Any],
53
+ image_data: Optional[list[Any]] = None,
54
+ ) -> tuple[list[Any], list[Any], Any] | tuple[Sequence[int], list[float], bool]:
55
+ """Generate tokens from prompt ids, used for async partial.
56
+
57
+ Args:
58
+ request_id (str): request id for sticky session.
59
+ prompt_ids (List[int]): List of prompt token ids.
60
+ sampling_params (Dict[str, Any]): Sampling parameters for the chat completion.
61
+
62
+ Returns:
63
+ output: A tuple representing the generation output.
64
+ - Element 0 (Sequence[int]): Generated response token IDs.
65
+ - Element 1 (list[float]): Log probabilities for the response token IDs.
66
+ - Element 2 (bool): A flag or status indicating cancellation.
67
+ """
68
+ server = self._choose_server(request_id)
69
+ output = await server.generate_for_partial.remote(
70
+ request_id=request_id,
71
+ prompt_ids=prompt_ids,
72
+ sampling_params=sampling_params,
73
+ image_data=image_data,
74
+ )
75
+ return output
76
+
77
+
78
+ @ray.remote
79
+ class FullyAsyncAgentLoopWorker(AgentLoopWorker):
80
+ def __init__(
81
+ self, config: DictConfig, server_handles: list[ray.actor.ActorHandle], reward_router_address: str = None
82
+ ):
83
+ self.server_manager = FullyAsyncLLMServerManager(config, server_handles)
84
+ super().__init__(config, server_handles, reward_router_address)
85
+ # A shared cancellation event for all agent loops running on this worker.
86
+ self.cancellation_event = asyncio.Event()
87
+
88
+ async def generate_sequences_no_post(
89
+ self, batch: DataProto, partial_output_list: Optional[list[AgentLoopOutput]]
90
+ ) -> tuple[list[AgentLoopOutput], bool] | tuple[DataProto, bool]:
91
+ """Generate sequences from agent loop.
92
+
93
+ Args:
94
+ batch (DataProto): Input batch.
95
+ partial_output_list: Optional[List[AgentLoopOutput]]: already rollout result.
96
+
97
+ Returns:
98
+ list[AgentLoopOutput]: List of agent loop outputs, one per sample in the batch.
99
+ """
100
+ config = self.config.actor_rollout_ref.rollout
101
+ sampling_params = dict(
102
+ temperature=config.temperature,
103
+ top_p=config.top_p,
104
+ repetition_penalty=1.0,
105
+ logprobs=config.calculate_log_probs,
106
+ )
107
+
108
+ # override sampling params for validation
109
+ if batch.meta_info.get("validate", False):
110
+ sampling_params["top_p"] = config.val_kwargs.top_p
111
+ sampling_params["temperature"] = config.val_kwargs.temperature
112
+
113
+ if "agent_name" not in batch.non_tensor_batch:
114
+ default_agent_loop = config.agent.default_agent_loop
115
+ batch.non_tensor_batch["agent_name"] = np.array([default_agent_loop] * len(batch), dtype=object)
116
+
117
+ if "index" in batch.non_tensor_batch:
118
+ index = batch.non_tensor_batch["index"]
119
+ else:
120
+ index = np.arange(len(batch))
121
+
122
+ trajectory_info = await get_trajectory_info(
123
+ batch.meta_info.get("global_steps", -1), index, batch.meta_info.get("validate", False)
124
+ )
125
+
126
+ if not partial_output_list:
127
+ partial_output_list = [None] * len(batch)
128
+ try:
129
+ tasks = []
130
+ for i in range(len(batch)):
131
+ kwargs = {k: v[i] for k, v in batch.non_tensor_batch.items()}
132
+ kwargs["output"] = partial_output_list[i]
133
+ tasks.append(
134
+ asyncio.create_task(self._partial_run_agent_loop(sampling_params, trajectory_info[i], **kwargs))
135
+ )
136
+ outputs = await asyncio.gather(*tasks)
137
+ except Exception:
138
+ logger.exception("_partial_run_agent_loop failed")
139
+ raise
140
+
141
+ is_cancel = any(output.extra_fields.get("is_cancel", False) for output in outputs)
142
+ if not is_cancel:
143
+ output = self._postprocess(outputs)
144
+ output = self._addition_process(output)
145
+ return output, is_cancel
146
+ return outputs, is_cancel
147
+
148
+ def _addition_process(self, output: DataProto):
149
+ """collect metirics"""
150
+ metrics = output.meta_info.pop("metrics") # List[Dict[str, str]]
151
+ processing_times_list = [item["generate_sequences"] for item in metrics]
152
+ tool_calls_times_list = [item["tool_calls"] for item in metrics]
153
+ output.non_tensor_batch["processing_times"] = processing_times_list
154
+ output.non_tensor_batch["tool_calls_times"] = tool_calls_times_list
155
+ return output
156
+
157
+ async def _partial_run_agent_loop(
158
+ self,
159
+ sampling_params: dict[str, Any],
160
+ trajectory: dict[str, Any],
161
+ *,
162
+ agent_name: str,
163
+ **kwargs,
164
+ ) -> AgentLoopOutput:
165
+ # Completed, return directly
166
+ if kwargs["output"] is not None and not kwargs["output"].extra_fields.get("is_cancel", False):
167
+ logger.info("In _partial_run_agent_loop, already completed, return derictly!")
168
+ return kwargs["output"]
169
+ try:
170
+ with rollout_trace_attr(
171
+ step=trajectory["step"],
172
+ sample_index=trajectory["sample_index"],
173
+ rollout_n=trajectory["rollout_n"],
174
+ validate=trajectory["validate"],
175
+ name="agent_loop",
176
+ ):
177
+ assert agent_name in _agent_loop_registry, (
178
+ f"Agent loop {agent_name} not registered, registered agent loops: {_agent_loop_registry.keys()}"
179
+ )
180
+
181
+ agent_loop_config = _agent_loop_registry[agent_name]
182
+ agent_loop = hydra.utils.instantiate(
183
+ config=agent_loop_config,
184
+ trainer_config=DictConfigWrap(config=self.config),
185
+ server_manager=self.server_manager,
186
+ tokenizer=self.tokenizer,
187
+ processor=self.processor,
188
+ dataset_cls=self.dataset_cls,
189
+ dataset_config=self.config.data,
190
+ )
191
+ output: AgentLoopOutput = await agent_loop.run(
192
+ sampling_params, cancellation_event=self.cancellation_event, **kwargs
193
+ )
194
+ if not output.extra_fields.get("is_cancel", False):
195
+ kwargs.pop("output", None)
196
+ output = await self._agent_loop_postprocess(output, **kwargs)
197
+
198
+ return output
199
+ except Exception:
200
+ logger.exception("Agent_loop run failed")
201
+ raise
202
+
203
+ async def cancel_agent_loops(self):
204
+ """Set the shared cancellation event to stop all agent loops."""
205
+ self.cancellation_event.set()
206
+
207
+ async def resume_agent_loops(self):
208
+ """Clear the shared cancellation event."""
209
+ self.cancellation_event.clear()
210
+
211
+
212
+ class FullyAsyncAgentLoopManager(AgentLoopManager):
213
+ def __init__(
214
+ self, config: DictConfig, worker_group: RayWorkerGroup = None, rm_resource_pool: RayResourcePool = None
215
+ ):
216
+ self.config = config
217
+ self.worker_group = worker_group
218
+ self.reward_model_manager = None
219
+ self.reward_router_address = None
220
+ self.agent_loop_workers_class = FullyAsyncAgentLoopWorker
221
+
222
+ # Select rollout replica class based on rollout name
223
+ rollout_name = config.actor_rollout_ref.rollout.name
224
+ if rollout_name == "sglang":
225
+ from verl.experimental.fully_async_policy.sglang_rollout.sglang_async_server import FullyAsyncSGLangReplica
226
+
227
+ self.rollout_replica_class = FullyAsyncSGLangReplica
228
+ print("[FullyAsyncAgentLoopManager] SGLang replica class selected")
229
+ elif rollout_name == "vllm":
230
+ from verl.experimental.fully_async_policy.vllm_rollout.vllm_async_server import FullyAsyncvLLMReplica
231
+
232
+ self.rollout_replica_class = FullyAsyncvLLMReplica
233
+ print("[FullyAsyncAgentLoopManager] vLLM replica class selected")
234
+ else:
235
+ raise ValueError(f"Unsupported rollout name: {rollout_name}. Supported values are 'sglang' and 'vllm'.")
236
+
237
+ self.rm_resource_pool = rm_resource_pool
238
+ self.rollout_replicas = None
239
+ self.server_handles = None
240
+ self.server_addresses = None
241
+ self.agent_loop_workers = None
242
+
243
+ @classmethod
244
+ async def create(
245
+ cls, config: DictConfig, worker_group: RayWorkerGroup = None, rm_resource_pool: RayResourcePool = None
246
+ ):
247
+ instance = cls(config, worker_group, rm_resource_pool)
248
+ await instance._async_init()
249
+ return instance
250
+
251
+ async def _async_init(self):
252
+ if self.config.reward_model.enable and self.config.reward_model.enable_resource_pool:
253
+ from verl.experimental.reward_loop import RewardModelManager
254
+
255
+ self.reward_model_manager = RewardModelManager(self.config.reward_model, self.rm_resource_pool)
256
+ self.reward_router_address = self.reward_model_manager.get_router_address()
257
+
258
+ await self._initialize_llm_servers_async()
259
+ self._init_agent_loop_workers()
260
+
261
+ async def _initialize_llm_servers_async(self):
262
+ rollout_world_size = (
263
+ self.config.actor_rollout_ref.rollout.tensor_model_parallel_size
264
+ * self.config.actor_rollout_ref.rollout.data_parallel_size
265
+ * self.config.actor_rollout_ref.rollout.pipeline_model_parallel_size
266
+ )
267
+ world_size = (
268
+ self.worker_group.world_size
269
+ if self.worker_group
270
+ else self.config.trainer.n_gpus_per_node * self.config.trainer.nnodes
271
+ )
272
+ num_replicas = world_size // rollout_world_size
273
+
274
+ rollout_config = self.config.actor_rollout_ref.rollout
275
+ model_config = self.config.actor_rollout_ref.model
276
+ self.rollout_replicas = [
277
+ self.rollout_replica_class(
278
+ replica_rank=replica_rank,
279
+ config=rollout_config,
280
+ model_config=model_config,
281
+ gpus_per_node=self.config.trainer.n_gpus_per_node,
282
+ )
283
+ for replica_rank in range(num_replicas)
284
+ ]
285
+
286
+ if self.worker_group:
287
+ await asyncio.gather(*[server.init_hybrid(self.worker_group) for server in self.rollout_replicas])
288
+ else:
289
+ await asyncio.gather(*[server.init_standalone() for server in self.rollout_replicas])
290
+
291
+ self.server_handles = [server._server_handle for server in self.rollout_replicas]
292
+ self.server_addresses = [server._server_address for server in self.rollout_replicas]
293
+
294
+ print(f"AgentLoopManager: {self.server_addresses}")
295
+ # Update Prometheus configuration with server addresses
296
+ if rollout_config.prometheus.enable:
297
+ if rollout_config.disable_log_stats:
298
+ raise ValueError("PROMETHEUS needs disable_log_stats==False, but it is currently True.")
299
+ await asyncio.to_thread(
300
+ update_prometheus_config, rollout_config.prometheus, self.server_addresses, rollout_config.name
301
+ )
302
+
303
+ async def generate_single_sample_async(
304
+ self,
305
+ sample: DataProto,
306
+ partial_output_list: Optional[list[AgentLoopOutput]],
307
+ ) -> tuple[list[AgentLoopOutput], bool] | tuple[DataProto, bool]:
308
+ """
309
+ Asynchronously process a single sample
310
+
311
+ Args:
312
+ sample: Single sample data
313
+ partial_output_list: Optional[List[AgentLoopOutput]]: already rollout result.
314
+
315
+ Returns:
316
+ list[AgentLoopOutput]: Processing results
317
+ """
318
+ worker = self._select_best_worker()
319
+ output_future = worker.generate_sequences_no_post.remote(sample, partial_output_list)
320
+ return await asyncio.wrap_future(output_future.future())
321
+
322
+ def _select_best_worker(self):
323
+ """Select the best worker, simple round-robin load balancing"""
324
+ if not hasattr(self, "_worker_index"):
325
+ self._worker_index = 0
326
+
327
+ worker = self.agent_loop_workers[self._worker_index]
328
+ self._worker_index = (self._worker_index + 1) % len(self.agent_loop_workers)
329
+ return worker
330
+
331
+ async def cancel(self):
332
+ worker_cancel_tasks = [worker.cancel_agent_loops.remote() for worker in self.agent_loop_workers]
333
+ rollout_cancel_tasks = [replica.cancel() for replica in self.rollout_replicas]
334
+ await asyncio.gather(*rollout_cancel_tasks, *worker_cancel_tasks)
335
+
336
+ async def resume(self):
337
+ rollout_resume_tasks = [replica.resume() for replica in self.rollout_replicas]
338
+ worker_resume_tasks = [worker.resume_agent_loops.remote() for worker in self.agent_loop_workers]
339
+ await asyncio.gather(*rollout_resume_tasks, *worker_resume_tasks)
340
+
341
+ async def wake_up(self):
342
+ await asyncio.gather(*[replica.wake_up() for replica in self.rollout_replicas])
343
+
344
+ async def sleep(self):
345
+ await asyncio.gather(*[replica.sleep() for replica in self.rollout_replicas])
346
+
347
+ async def reset_prefix_cache(self):
348
+ print("[FullyAsyncAgentLoopManager] Reset prefix cache ...")
349
+ # await asyncio.gather(*[replica.reset_prefix_cache() for replica in self.rollout_replicas])
350
+ # Note: debug
351
+ timeout = 5.0
352
+
353
+ async def reset_one(idx, replica):
354
+ print(f"[reset_prefix_cache] start replica={idx}")
355
+ try:
356
+ await asyncio.wait_for(replica.reset_prefix_cache(), timeout=timeout)
357
+ except asyncio.TimeoutError:
358
+ print(f"[reset_prefix_cache] TIMEOUT replica={idx} after {timeout}s")
359
+ return
360
+ except Exception as e:
361
+ print(f"[reset_prefix_cache] ERROR replica={idx}: {e!r}")
362
+ return
363
+ print(f"[reset_prefix_cache] done replica={idx}")
364
+
365
+ tasks = [reset_one(i, replica) for i, replica in enumerate(self.rollout_replicas)]
366
+ await asyncio.gather(*tasks, return_exceptions=True)
367
+ print("[FullyAsyncAgentLoopManager] Reset prefix cache finished")
368
+
369
+ async def clear_kv_cache(self):
370
+ await asyncio.gather(*[replica.clear_kv_cache() for replica in self.rollout_replicas])
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_single_turn_agent_loop.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import logging
15
+ import os
16
+ from typing import Any, Optional
17
+ from uuid import uuid4
18
+
19
+ from verl.experimental.agent_loop import AgentLoopBase
20
+ from verl.experimental.agent_loop.agent_loop import AgentLoopOutput, register
21
+ from verl.utils.profiler import simple_timer
22
+
23
+ logger = logging.getLogger(__file__)
24
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
25
+
26
+
27
+ @register("partial_single_turn_agent")
28
+ class PartialSingleTurnAgentLoop(AgentLoopBase):
29
+ """Naive agent loop that only do single turn chat completion."""
30
+
31
+ def __init__(self, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+ self.prompt_length = self.config.actor_rollout_ref.rollout.prompt_length
34
+ self.response_length = self.config.actor_rollout_ref.rollout.response_length
35
+ self.apply_chat_template_kwargs = self.config.data.get("apply_chat_template_kwargs", {})
36
+
37
+ async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
38
+ output: Optional[AgentLoopOutput] = kwargs.get("output", None)
39
+ messages = list(kwargs["raw_prompt"])
40
+ param_version = kwargs.get("param_version", 0)
41
+
42
+ metrics = {}
43
+ request_id = uuid4().hex
44
+ image_data = (kwargs.get("multi_modal_data") or {}).get("image", None)
45
+
46
+ param_version_start = param_version
47
+ param_version_end = param_version
48
+
49
+ if not output:
50
+ # TODO(baiyan): it is supposed to use the correct processor,
51
+ # but I found the async training would hang if use_correct_processor=True.
52
+ # so we use the tokenizer to tokenize the prompt for now.
53
+ use_correct_processor = False
54
+ if self.processor is not None and use_correct_processor:
55
+
56
+ def get_prompt_ids():
57
+ raw_prompt = self.processor.apply_chat_template(
58
+ messages,
59
+ add_generation_prompt=True,
60
+ tokenize=False,
61
+ **self.apply_chat_template_kwargs,
62
+ )
63
+ model_inputs = self.processor(text=[raw_prompt], images=image_data, return_tensors="pt")
64
+ return model_inputs.pop("input_ids").squeeze(0).tolist()
65
+
66
+ prompt_ids = await self.loop.run_in_executor(None, get_prompt_ids)
67
+ else:
68
+ prompt_ids = await self.loop.run_in_executor(
69
+ None,
70
+ lambda: self.tokenizer.apply_chat_template(
71
+ messages, add_generation_prompt=True, tokenize=True, **self.apply_chat_template_kwargs
72
+ ),
73
+ )
74
+ else:
75
+ if output.extra_fields.get("is_cancel", False):
76
+ # Resume the paused sample,
77
+ # add the result directly after prompt_ids,
78
+ # and reset generate_sequences metric
79
+ prompt_ids = output.prompt_ids + output.response_ids
80
+ metrics["generate_sequences"] = output.metrics.generate_sequences
81
+ param_version_start = output.extra_fields.get("param_version_start", param_version)
82
+ else:
83
+ # In the same batch of samples,
84
+ # some are canceled and some are not.
85
+ # The samples without partial rollout are returned directly.
86
+ return output
87
+ with simple_timer("generate_sequences", metrics):
88
+ response_ids, response_logprobs, is_cancel = await self.server_manager.generate_for_partial(
89
+ request_id=request_id, prompt_ids=prompt_ids, sampling_params=sampling_params, image_data=image_data
90
+ )
91
+ if not output:
92
+ response_mask = [1] * len(response_ids)
93
+ else:
94
+ # Pause the sample to be resumed, add the output result to response_ids, and reset response_mask
95
+ prompt_ids = output.prompt_ids
96
+ response_logprobs = output.response_logprobs + response_logprobs
97
+ response_ids = output.response_ids + response_ids
98
+ response_mask = [1] * len(response_ids)
99
+ if len(response_ids) >= self.response_length:
100
+ is_cancel = False
101
+
102
+ return AgentLoopOutput(
103
+ prompt_ids=prompt_ids,
104
+ response_ids=response_ids[: self.response_length],
105
+ response_mask=response_mask[: self.response_length],
106
+ response_logprobs=response_logprobs[: self.response_length],
107
+ num_turns=2,
108
+ metrics=metrics,
109
+ extra_fields={
110
+ "is_cancel": is_cancel,
111
+ "param_version_start": param_version_start,
112
+ "param_version_end": param_version_end,
113
+ },
114
+ # multi_modal_data={"image": image_data} if image_data is not None else {},
115
+ )
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_tool_agent_loop.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import copy
17
+ import logging
18
+ import os
19
+ from typing import Any, Optional
20
+ from uuid import uuid4
21
+
22
+ from verl.experimental.agent_loop.agent_loop import AgentLoopOutput, register
23
+ from verl.experimental.agent_loop.tool_agent_loop import AgentData, AgentState, ToolAgentLoop
24
+ from verl.utils.profiler import simple_timer
25
+
26
+ logger = logging.getLogger(__file__)
27
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
28
+
29
+
30
+ @register("async_partial_tool_agent")
31
+ class AsyncPartialToolAgentLoop(ToolAgentLoop):
32
+ """
33
+ Support for partial rollout with multiple tool invocations in Agent Loop
34
+
35
+ """
36
+
37
+ def __init__(self, trainer_config, **kwargs):
38
+ super().__init__(trainer_config, **kwargs)
39
+ self.enable_partial_rollout = trainer_config.config.async_training.get("partial_rollout", False)
40
+
41
+ # async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput:
42
+ async def run(
43
+ self, sampling_params: dict[str, Any], *, cancellation_event: asyncio.Event = None, **kwargs
44
+ ) -> AgentLoopOutput:
45
+ """
46
+ Main entrance, supports interruption/recovery
47
+
48
+ Args:
49
+ sampling_params: Sampling parameters
50
+ cancellation_event: cancellationn sginal
51
+ **kwargs: Contains output (for recovery), raw_prompt, param_version, etc.
52
+
53
+ Returns:
54
+ AgentLoopOutput: Include the is_cancel flag
55
+ """
56
+ param_version = kwargs.get("param_version", 0)
57
+ agent_data = None
58
+ state = None
59
+
60
+ # 1. check whether is the partial task
61
+ output: Optional[AgentLoopOutput] = kwargs.get("output", None)
62
+ if output and output.extra_fields.get("is_cancel", False):
63
+ agent_data, state = self._restore_from_output(output)
64
+
65
+ logger.info(f"[PartialToolAgent] Resuming from {state.value}")
66
+ else:
67
+ if output and not output.extra_fields.get("is_cancel", False):
68
+ # Completed, return directly
69
+ return output
70
+
71
+ agent_data = await self._init_agent_data(kwargs, param_version)
72
+ state = AgentState.PENDING
73
+ logger.info("[PartialToolAgent] Start from scratch")
74
+ # 2. run state machine
75
+ state = await self._run_state_machine(agent_data, state, sampling_params, cancellation_event)
76
+
77
+ # 3. bulid output
78
+ if state == AgentState.TERMINATED:
79
+ return self._build_completed_output(agent_data, param_version)
80
+ else:
81
+ # build cancelled output
82
+ return self._build_cancelled_output(agent_data, state)
83
+
84
+ async def _init_agent_data(self, kwargs: dict, param_version: int) -> AgentData:
85
+ messages = list(kwargs["raw_prompt"])
86
+ image_data = copy.deepcopy(kwargs.get("multi_modal_data", {}).get("image", None))
87
+ video_data = copy.deepcopy(kwargs.get("multi_modal_data", {}).get("video", None))
88
+ metrics = {}
89
+ request_id = uuid4().hex
90
+ tools_kwargs = kwargs.get("tools_kwargs", {})
91
+
92
+ # Initialize interaction if needed
93
+ interaction = None
94
+ interaction_kwargs = {}
95
+ if self.interaction_config_file:
96
+ interaction_kwargs = kwargs["extra_info"]["interaction_kwargs"]
97
+ if "name" not in interaction_kwargs:
98
+ raise ValueError("'name' key is required in interaction_kwargs")
99
+ interaction_name = interaction_kwargs["name"]
100
+ if interaction_name not in self.interaction_map:
101
+ raise ValueError(
102
+ f"Interaction '{interaction_name}' not found in interaction_map. Available interactions: "
103
+ f"{list(self.interaction_map.keys())}"
104
+ )
105
+ interaction = self.interaction_map[interaction_name]
106
+ await interaction.start_interaction(request_id, **interaction_kwargs)
107
+ # Create AgentData instance to encapsulate all state
108
+ agent_data = AgentData(
109
+ messages=messages,
110
+ image_data=image_data,
111
+ video_data=video_data,
112
+ metrics=metrics,
113
+ request_id=request_id,
114
+ tools_kwargs=tools_kwargs,
115
+ interaction=interaction,
116
+ interaction_kwargs=interaction_kwargs,
117
+ )
118
+
119
+ # additional param version record
120
+ agent_data.extra_fields["param_version_start"] = param_version
121
+ agent_data.extra_fields["param_version_end"] = param_version
122
+
123
+ return agent_data
124
+
125
+ def _restore_from_output(self, output: AgentLoopOutput) -> tuple[AgentData, AgentState]:
126
+ """restore AgentState and AgentData from output"""
127
+ agent_data = output.extra_fields.get("agent_data", None)
128
+ agent_state = output.extra_fields.get("agent_state", None)
129
+ if agent_data is None or agent_state is None:
130
+ raise ValueError(f"Unexpected situation: agent_data is {agent_data}, agent_state is {agent_state}")
131
+ return agent_data, agent_state
132
+
133
+ async def _run_state_machine(
134
+ self,
135
+ agent_data: AgentData,
136
+ state: AgentState,
137
+ sampling_params: dict[str, Any],
138
+ cancellation_event: asyncio.Event = None,
139
+ ) -> AgentState:
140
+ """
141
+ State machine.
142
+ Currently, interruptions are only supported to occur in the GENERATING state or other states have ended.
143
+ """
144
+ # State machine loop
145
+ while state != AgentState.TERMINATED:
146
+ if cancellation_event and cancellation_event.is_set():
147
+ logger.info(f"[PartialToolAgent] Cancellation detected. Interrupted before/at state: {state.value}")
148
+ return state
149
+ if state == AgentState.PENDING:
150
+ state = await self._handle_pending_state(agent_data, sampling_params)
151
+ elif state == AgentState.GENERATING:
152
+ state = await self._handle_generating_state_partial(agent_data, sampling_params)
153
+ elif state == AgentState.PROCESSING_TOOLS:
154
+ state = await self._handle_processing_tools_state(agent_data)
155
+ elif state == AgentState.INTERACTING:
156
+ state = await self._handle_interacting_state(agent_data)
157
+ else:
158
+ logger.error(f"[PartialToolAgent] Invalid state: {state}")
159
+ return AgentState.TERMINATED
160
+
161
+ return AgentState.TERMINATED
162
+
163
+ async def _handle_generating_state_partial(
164
+ self, agent_data: AgentData, sampling_params: dict[str, Any], ignore_termination: bool = False
165
+ ) -> AgentState:
166
+ """
167
+ Handle GENERATING state, support partial rollout
168
+ """
169
+ add_messages: list[dict[str, Any]] = []
170
+
171
+ with simple_timer("generate_sequences", agent_data.metrics):
172
+ # partial interface
173
+ if self.enable_partial_rollout:
174
+ response_ids, log_probs, is_cancel = await self.server_manager.generate_for_partial(
175
+ request_id=agent_data.request_id,
176
+ prompt_ids=agent_data.prompt_ids,
177
+ sampling_params=sampling_params,
178
+ image_data=agent_data.image_data,
179
+ )
180
+
181
+ if is_cancel:
182
+ # Save the generated parts
183
+ agent_data.response_ids = response_ids
184
+ agent_data.prompt_ids += agent_data.response_ids
185
+ agent_data.response_mask += [1] * len(response_ids)
186
+ if log_probs:
187
+ agent_data.response_logprobs += log_probs
188
+ if not ignore_termination and len(agent_data.response_mask) >= self.response_length:
189
+ # If response_length has reached the limit,
190
+ # it is considered to have ended normally.
191
+ agent_data.assistant_turns += 1
192
+ return AgentState.TERMINATED
193
+ return AgentState.GENERATING
194
+ else:
195
+ # original generate interface
196
+ output = await self.server_manager.generate(
197
+ request_id=agent_data.request_id,
198
+ prompt_ids=agent_data.prompt_ids,
199
+ sampling_params=sampling_params,
200
+ image_data=agent_data.image_data,
201
+ )
202
+ response_ids = output.token_ids
203
+ log_probs = output.log_probs
204
+
205
+ agent_data.assistant_turns += 1
206
+ agent_data.response_ids = response_ids
207
+ agent_data.prompt_ids += agent_data.response_ids
208
+ agent_data.response_mask += [1] * len(agent_data.response_ids)
209
+ if log_probs:
210
+ agent_data.response_logprobs += log_probs
211
+
212
+ if not ignore_termination and len(agent_data.response_mask) >= self.response_length:
213
+ return AgentState.TERMINATED
214
+ if self.max_assistant_turns and agent_data.assistant_turns >= self.max_assistant_turns:
215
+ return AgentState.TERMINATED
216
+ if self.max_user_turns and agent_data.user_turns >= self.max_user_turns:
217
+ return AgentState.TERMINATED
218
+
219
+ # Extract tool calls
220
+ _, agent_data.tool_calls = await self.tool_parser.extract_tool_calls(agent_data.response_ids)
221
+
222
+ # Handle interaction if needed
223
+ if self.interaction_config_file:
224
+ assistant_message = await self.loop.run_in_executor(
225
+ None, lambda: self.tokenizer.decode(agent_data.response_ids, skip_special_tokens=True)
226
+ )
227
+ add_messages.append({"role": "assistant", "content": assistant_message})
228
+ agent_data.messages.extend(add_messages)
229
+
230
+ # Determine next state
231
+ if agent_data.tool_calls:
232
+ return AgentState.PROCESSING_TOOLS
233
+ elif self.interaction_config_file:
234
+ return AgentState.INTERACTING
235
+ else:
236
+ return AgentState.TERMINATED
237
+
238
+ def _build_completed_output(self, agent_data: AgentData, param_version: int) -> AgentLoopOutput:
239
+ """build completed output"""
240
+ response_ids = agent_data.prompt_ids[-len(agent_data.response_mask) :]
241
+ prompt_ids = agent_data.prompt_ids[: len(agent_data.prompt_ids) - len(agent_data.response_mask)]
242
+ multi_modal_data = {"image": agent_data.image_data} if agent_data.image_data is not None else {}
243
+ output = AgentLoopOutput(
244
+ prompt_ids=prompt_ids,
245
+ response_ids=response_ids[: self.response_length],
246
+ response_mask=agent_data.response_mask[: self.response_length],
247
+ multi_modal_data=multi_modal_data,
248
+ response_logprobs=agent_data.response_logprobs[: self.response_length]
249
+ if agent_data.response_logprobs
250
+ else None,
251
+ num_turns=agent_data.user_turns + agent_data.assistant_turns + 1,
252
+ metrics=agent_data.metrics,
253
+ extra_fields={},
254
+ )
255
+ output.extra_fields.update(
256
+ {
257
+ "turn_scores": agent_data.turn_scores,
258
+ "tool_rewards": agent_data.tool_rewards,
259
+ "is_cancel": False,
260
+ "param_version_start": agent_data.extra_fields["param_version_start"],
261
+ "param_version_end": param_version,
262
+ }
263
+ )
264
+ return output
265
+
266
+ def _build_cancelled_output(self, agent_data: AgentData, state: AgentState) -> AgentLoopOutput:
267
+ """build cancelled output"""
268
+ return AgentLoopOutput(
269
+ prompt_ids=[],
270
+ response_ids=[],
271
+ response_mask=[],
272
+ multi_modal_data={},
273
+ response_logprobs=None,
274
+ num_turns=0,
275
+ metrics=agent_data.metrics,
276
+ extra_fields={
277
+ "is_cancel": True,
278
+ "agent_data": agent_data,
279
+ "agent_state": state,
280
+ },
281
+ )
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/base_detach_sync.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import asyncio
17
+ import logging
18
+ import os
19
+ import threading
20
+
21
+ import torch
22
+ from omegaconf import DictConfig
23
+ from ray.util.collective import collective
24
+
25
+ from verl.single_controller.base.decorator import Dispatch, register
26
+ from verl.utils.device import get_torch_device, is_npu_available
27
+ from verl.utils.distributed import stateless_init_process_group
28
+
29
+ logger = logging.getLogger(__file__)
30
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
31
+
32
+
33
+ class BaseDetachNcclSync:
34
+ _bucket_size_mb = 1024.0
35
+ _sync_history = []
36
+ _max_history_size = 20
37
+ _last_avg_bucket_size = 1024.0
38
+
39
+ def __init__(self, config: DictConfig, role: str):
40
+ self._bg_loop = asyncio.new_event_loop()
41
+ self._bg_thread = threading.Thread(
42
+ target=self._start_background_loop, args=(self._bg_loop,), name="rollout_actor_async_worker", daemon=True
43
+ )
44
+ self._bg_thread.start()
45
+ logger.info(f"[DetachNcclSync] Background thread for SGLang sync started. PID: {os.getpid()}")
46
+
47
+ @classmethod
48
+ def get_bucket_size_mb(cls):
49
+ return cls._bucket_size_mb
50
+
51
+ @classmethod
52
+ def get_last_avg_bucket_size(cls):
53
+ return cls._last_avg_bucket_size
54
+
55
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=True)
56
+ def get_last_avg_bucket_size_remote(self):
57
+ return BaseDetachNcclSync._last_avg_bucket_size
58
+
59
+ @classmethod
60
+ def record_sync_metrics(cls, bucket_size_mb, sync_time):
61
+ """Dynamically adjust the bucket size based on past synchronization times."""
62
+ bucket_size_mb_value = bucket_size_mb[0] if isinstance(bucket_size_mb, list) else bucket_size_mb
63
+ print(f"[DetachNcclSync] sync_metrics: bucket_size_mb={bucket_size_mb_value:.2f}MB, sync_time={sync_time:.2f}s")
64
+ cls._sync_history.append((bucket_size_mb_value, sync_time))
65
+ if len(cls._sync_history) > cls._max_history_size:
66
+ cls._sync_history.pop(0)
67
+
68
+ MIN_BUCKET_SIZE_MB = 512
69
+ MAX_BUCKET_SIZE_MB = 8192 # 8GB
70
+
71
+ if len(cls._sync_history) < 4:
72
+ cls._bucket_size_mb = min(MAX_BUCKET_SIZE_MB, cls._bucket_size_mb * 1.5)
73
+ else:
74
+ times = [t for _, t in cls._sync_history]
75
+ buckets = [b for b, _ in cls._sync_history]
76
+ recent_avg_time = sum(times[-2:]) / 2
77
+ previous_avg_time = sum(times[-4:-2]) / 2
78
+ recent_avg_bucket = sum(buckets[-2:]) / 2
79
+ previous_avg_bucket = sum(buckets[-4:-2]) / 2
80
+
81
+ performance_improved = recent_avg_time < previous_avg_time
82
+ bucket_increased = recent_avg_bucket > previous_avg_bucket
83
+ time_change_ratio = (
84
+ abs(recent_avg_time - previous_avg_time) / previous_avg_time if previous_avg_time > 0 else 0.0
85
+ )
86
+
87
+ if time_change_ratio > 0.2:
88
+ increase_step, decrease_step = 1.2, 0.8
89
+ elif time_change_ratio > 0.1:
90
+ increase_step, decrease_step = 1.1, 0.9
91
+ elif time_change_ratio > 0.05:
92
+ increase_step, decrease_step = 1.05, 0.95
93
+ else:
94
+ increase_step, decrease_step = 1.02, 0.98
95
+
96
+ should_increase = (performance_improved and bucket_increased) or (
97
+ not performance_improved and not bucket_increased
98
+ )
99
+ step = increase_step if should_increase else decrease_step
100
+ new_size = cls._bucket_size_mb * step
101
+ cls._bucket_size_mb = min(MAX_BUCKET_SIZE_MB, max(MIN_BUCKET_SIZE_MB, new_size))
102
+
103
+ def _start_background_loop(self, loop):
104
+ asyncio.set_event_loop(loop)
105
+ try:
106
+ loop.run_forever()
107
+ except Exception as e:
108
+ logger.error(f"[DetachNcclSync] Background loop crashed: {e}")
109
+
110
+ def _run_async_safely(self, coro):
111
+ if not self._bg_thread.is_alive():
112
+ raise RuntimeError("Background thread for SGLang sync is not running!")
113
+
114
+ future = asyncio.run_coroutine_threadsafe(coro, self._bg_loop)
115
+ return future.result()
116
+
117
+ def __del__(self):
118
+ if hasattr(self, "_bg_loop") and self._bg_loop.is_running():
119
+ self._bg_loop.call_soon_threadsafe(self._bg_loop.stop)
120
+ if hasattr(self, "_bg_thread") and self._bg_thread.is_alive():
121
+ self._bg_thread.join(timeout=1.0)
122
+
123
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
124
+ def init_checkpoint_engine(self, rank_offset: int, actor_num: int, rollout_num: int):
125
+ from .checkpoint_engine import CheckpointEngine
126
+
127
+ current_rank = torch.distributed.get_rank() + rank_offset
128
+ actor_ranks = list(range(actor_num))
129
+ rollout_ranks = [rank + actor_num for rank in range(rollout_num)]
130
+ assert rank_offset == 0 or rank_offset == actor_num
131
+
132
+ self.checkpoint_engine = CheckpointEngine(
133
+ current_rank, actor_ranks, rollout_ranks, self.config.checkpoint_engine.device_buffer_size_M
134
+ )
135
+
136
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
137
+ def create_weight_sync_group(self, master_address, master_port, rank_offset, world_size):
138
+ rank = torch.distributed.get_rank() + rank_offset
139
+ self._weight_sync_group = stateless_init_process_group(
140
+ master_address,
141
+ master_port,
142
+ rank,
143
+ world_size,
144
+ get_torch_device().current_device(),
145
+ )
146
+
147
+ @staticmethod
148
+ def get_inference_model(rollout):
149
+ """
150
+ Get models according to different types of inference_engine
151
+ Args:
152
+ rollout: rollout object
153
+ Returns:
154
+ model: model object (for vllm) or rollout object itself (for sglang)
155
+ """
156
+ inference_engine = rollout.inference_engine
157
+ if hasattr(inference_engine, "llm_engine"):
158
+ inference_model = inference_engine.llm_engine.model_executor.driver_worker.worker.model_runner.model
159
+ elif hasattr(inference_engine, "worker"):
160
+ inference_model = inference_engine.worker.model_runner.model
161
+ else:
162
+ raise AttributeError(
163
+ f"Unsupported inference_engine type: {type(inference_engine)}. "
164
+ f"Expected LLM (with llm_engine attribute) or WorkerWrapperBase (with worker attribute)."
165
+ )
166
+ return inference_model
167
+
168
+ def _sync_sglang_weights(self, inference_model, params, sync_group_name):
169
+ bucket_size_bytes = int(self.get_bucket_size_mb() * 1024 * 1024)
170
+ actual_bucket_sizes = []
171
+ current_batch = []
172
+ current_batch_size = 0
173
+
174
+ def flush_batch():
175
+ if current_batch:
176
+ actual_bucket_sizes.append(current_batch_size / (1024 * 1024))
177
+ self._run_async_safely(self.update_weights(inference_model, iter(current_batch)))
178
+ get_torch_device().synchronize()
179
+ current_batch.clear()
180
+
181
+ for key, shape, dtype in self._weights_info:
182
+ tensor = torch.empty(shape, dtype=dtype, device=get_torch_device().current_device())
183
+ if self._is_actor:
184
+ assert key in params
185
+ origin_data = params[key]
186
+ if hasattr(origin_data, "full_tensor"):
187
+ origin_data = origin_data.full_tensor()
188
+ if torch.distributed.get_rank() == 0:
189
+ tensor.copy_(origin_data)
190
+ collective.broadcast(tensor, src_rank=0, group_name=sync_group_name)
191
+
192
+ tensor_size = tensor.numel() * tensor.element_size()
193
+ current_batch.append((key, tensor))
194
+ current_batch_size += tensor_size
195
+
196
+ if current_batch_size >= bucket_size_bytes:
197
+ flush_batch()
198
+ current_batch_size = 0
199
+
200
+ flush_batch()
201
+ cls = type(self)
202
+ cls._last_avg_bucket_size = (
203
+ sum(actual_bucket_sizes) / len(actual_bucket_sizes) if actual_bucket_sizes else self.get_bucket_size_mb()
204
+ )
205
+
206
+ # Resume kv_cache after weights sync to restore GPU memory released during pause
207
+ if self._is_rollout and self.rollout_device_mesh["infer_tp"].get_local_rank() == 0:
208
+ self._run_async_safely(inference_model.resume_memory_occupation(tags=["kv_cache"]))
209
+
210
+ def _sync_vllm_weights(self, inference_model, params, sync_group_name):
211
+ for key, shape, dtype in self._weights_info:
212
+ tensor = torch.empty(shape, dtype=dtype, device=get_torch_device().current_device())
213
+ if self._is_actor:
214
+ assert key in params
215
+ origin_data = params[key]
216
+ if hasattr(origin_data, "full_tensor"):
217
+ origin_data = origin_data.full_tensor()
218
+ if torch.distributed.get_rank() == 0:
219
+ tensor.copy_(origin_data)
220
+ if is_npu_available:
221
+ self._weight_sync_group.broadcast(tensor, src=0, stream=get_torch_device().current_stream())
222
+ else:
223
+ collective.broadcast(tensor, src_rank=0, group_name=sync_group_name)
224
+ if self._is_rollout:
225
+ inference_model.load_weights([(key, tensor)])
226
+
227
+ async def update_weights(self, inference_engine, params):
228
+ from sglang.srt.weight_sync.utils import update_weights as sgl_update_weights
229
+
230
+ await sgl_update_weights(
231
+ engine=inference_engine,
232
+ params_batch=params,
233
+ device_mesh_key="infer_tp",
234
+ device_mesh=self.rollout_device_mesh,
235
+ )
236
+
237
+ if self.rollout_device_mesh["infer_tp"].get_local_rank() == 0:
238
+ await inference_engine.flush_cache()
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/checkpoint_engine.py ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ This logic is largely copied from:
16
+ - https://github.com/MoonshotAI/checkpoint-engine
17
+ """
18
+
19
+ import concurrent.futures
20
+ import os
21
+ import re
22
+ import socket
23
+ import subprocess
24
+ import threading
25
+ from collections.abc import Callable
26
+ from functools import lru_cache
27
+ from typing import TYPE_CHECKING, Annotated, Any, TypedDict
28
+
29
+ import torch
30
+ import zmq
31
+ from pydantic import BaseModel, PlainSerializer, PlainValidator, WithJsonSchema
32
+ from ray.util.collective import collective
33
+
34
+ from verl.utils.device import (
35
+ get_device_name,
36
+ get_torch_device,
37
+ )
38
+
39
+ if TYPE_CHECKING:
40
+ from typing import TypeVar
41
+
42
+ from typing_extensions import TypedDict
43
+
44
+ class FileMeta(TypedDict):
45
+ key: str # parameter name
46
+ dtype: torch.dtype
47
+ shape: torch.Size
48
+ type: type
49
+ tp_concat_dim: int
50
+
51
+ T = TypeVar("T")
52
+
53
+
54
+ def _dt_validate(value: Any) -> torch.dtype:
55
+ """Validate the input value to ensure it is a valid torch.dtype"""
56
+ if isinstance(value, str):
57
+ if not value.startswith("torch."):
58
+ raise ValueError(f"dtype {value} should start with torch.")
59
+ try:
60
+ value = getattr(torch, value.split(".")[1])
61
+ except AttributeError as e:
62
+ raise ValueError(f"unknown dtype: {value}") from e
63
+ if not isinstance(value, torch.dtype):
64
+ raise TypeError(f"dtype {value} should be torch.dtype, got {type(value)}")
65
+ return value
66
+
67
+
68
+ # Annotated type for torch.dtype with validation and serialization
69
+ _TorchDtype = Annotated[
70
+ torch.dtype,
71
+ PlainValidator(_dt_validate),
72
+ PlainSerializer(lambda x: str(x), return_type=str),
73
+ WithJsonSchema({"type": "string"}, mode="serialization"),
74
+ ]
75
+
76
+
77
+ def _size_validate(value: Any) -> torch.Size:
78
+ """Validate the input value to ensure it is a valid torch.Size"""
79
+ if isinstance(value, list | tuple):
80
+ return torch.Size(value)
81
+ if not isinstance(value, torch.Size):
82
+ raise TypeError(f"size {value} should be torch.Size, got {type(value)}")
83
+ return value
84
+
85
+
86
+ # Annotated type for torch.Size with validation and serialization
87
+ _TorchSize = Annotated[
88
+ torch.Size,
89
+ PlainValidator(_size_validate),
90
+ PlainSerializer(lambda x: tuple(x), return_type=tuple),
91
+ WithJsonSchema({"type": "array", "items": {"type": "integer"}}, mode="serialization"),
92
+ ]
93
+
94
+
95
+ def _tensor_validate(value: Any) -> torch.Tensor:
96
+ """Validate the input value to ensure it is a valid torch.Tensor"""
97
+ if isinstance(value, torch.Tensor):
98
+ return value
99
+ raise TypeError(f"tensor {value} should be torch.Tensor, got {type(value)}")
100
+
101
+
102
+ # Annotated type for torch.Tensor with validation
103
+ _TorchTensor = Annotated[
104
+ torch.Tensor,
105
+ PlainValidator(_tensor_validate),
106
+ ]
107
+
108
+
109
+ class ParameterMeta(BaseModel):
110
+ """Metadata for a parameter including name, dtype, and shape"""
111
+
112
+ name: str
113
+ dtype: _TorchDtype
114
+ shape: _TorchSize
115
+
116
+
117
+ class MemoryBuffer(BaseModel):
118
+ """
119
+ MemoryBuffer assembles a group of parameter tensors into a single buffer,
120
+ and records the meta information of each original parameter.
121
+ """
122
+
123
+ buffer: _TorchTensor
124
+ size: int # size of buffer in bytes
125
+ metas: list[ParameterMeta]
126
+
127
+
128
+ class MemoryBufferMeta(BaseModel):
129
+ """The meta info of MemoryBuffer, but not store the buffer data"""
130
+
131
+ size: int
132
+ metas: list[ParameterMeta]
133
+
134
+
135
+ # 256 bytes alignment when flatten torch tensors to uint8 buffer
136
+ _ALIGN_SIZE = 256
137
+
138
+
139
+ def _align_size(dtype: torch.dtype, shape: torch.Size) -> int:
140
+ """
141
+ Calculate the aligned size of a torch tensor
142
+
143
+ If the tensor's size (in bytes) cannot be evenly divided by _ALIGN_SIZE,
144
+ it will be rounded up to the nearest multiple of _ALIGN_SIZE.
145
+
146
+ Args:
147
+ dtype (torch.dtype): The data type of the tensor (e.g., torch.float32, torch.int64).
148
+ shape (torch.Size): The shape of the tensor, representing its dimensions.
149
+
150
+ Returns:
151
+ int: The aligned size of the tensor in bytes.
152
+ """
153
+ return (dtype.itemsize * shape.numel() + _ALIGN_SIZE - 1) // _ALIGN_SIZE * _ALIGN_SIZE
154
+
155
+
156
+ @lru_cache(maxsize=1)
157
+ def get_ip() -> str:
158
+ try:
159
+ # try to get ip from network interface
160
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
161
+ s.connect(("8.8.8.8", 80))
162
+ return s.getsockname()[0]
163
+ except Exception as e: # noqa: BLE001
164
+ # fallback to get ip from hostname
165
+ print(f"fail to get ip from network interface, fallback to get ip from hostname: {e}")
166
+ return socket.gethostbyname(socket.gethostname())
167
+
168
+
169
+ def npu_generate_uuid() -> str:
170
+ """Generate uuid for each npu device"""
171
+ str_pid = str(os.getpid())
172
+ npu_num = 8
173
+ try:
174
+ for npu_id in range(npu_num):
175
+ cmd = ["npu-smi", "info", "-t", "proc-mem", "-i", str(npu_id)]
176
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True) # noqa: S603
177
+ str_result = str(result.stdout)
178
+ if str_pid in str_result:
179
+ # In A3 server, one NPU has two chips.
180
+ match_chip_count = re.search(r"Chip Count[^\d]*(\d+)", str_result)
181
+ chip_count = int(match_chip_count.group(1))
182
+ search_after_pid = str_result[str_result.find(str_pid) + len(str_pid) :]
183
+ match_chip_id = re.search(r"Chip ID[^\d]*(\d+)", search_after_pid)
184
+ chip_id = int(match_chip_id.group(1))
185
+ return f"{get_ip()}-{npu_id * chip_count + chip_id}"
186
+ raise ValueError("The current process is not running on the npu device")
187
+ except subprocess.CalledProcessError as e:
188
+ raise ValueError("The current process is not running on the npu device") from e
189
+
190
+
191
+ def _get_physical_device_id(device_index: int | None = None) -> str:
192
+ """
193
+ Get the physical device (GPU or NPU) uuid of the current device
194
+ """
195
+ try:
196
+ if get_device_name() == "npu":
197
+ return f"NPU-{npu_generate_uuid()}"
198
+ else:
199
+ return f"GPU-{get_torch_device().get_device_properties(device_index).uuid!s}"
200
+ except AssertionError as e:
201
+ raise ValueError(f"fail to get physical gpu id {device_index}") from e
202
+
203
+
204
+ class FlattenedTensorMetadata(TypedDict):
205
+ name: str
206
+ shape: torch.Size
207
+ dtype: torch.dtype
208
+ # specify the start offset of this tensor in shared ipc_buffer tensor
209
+ offset: int
210
+
211
+
212
+ def _to_flattened_tensor_meta(metas: list[ParameterMeta], offset: int = 0) -> list[FlattenedTensorMetadata]:
213
+ """
214
+ compute the offset of each parameter in the buffer
215
+
216
+ Args:
217
+ metas (list[ParameterMeta]): The list of parameter metas info
218
+ offset (int): The start offset of the buffer. Defaults to 0.
219
+
220
+ Returns:
221
+ list[FlattenedTensorMetadata]: The list of FlattenedTensorMetadata:
222
+ """
223
+ ret = []
224
+ for meta in metas:
225
+ size = _align_size(meta.dtype, meta.shape)
226
+ ret.append(
227
+ {
228
+ "name": meta.name,
229
+ "dtype": meta.dtype,
230
+ "shape": meta.shape,
231
+ "offset": offset,
232
+ }
233
+ )
234
+ offset += size
235
+ return ret
236
+
237
+
238
+ def _extract_weights(
239
+ flatten_metas: list[FlattenedTensorMetadata], buffer: torch.Tensor
240
+ ) -> list[tuple[str, torch.Tensor]]:
241
+ """
242
+ According to the flatten_metas and buffer, extract the weights
243
+ """
244
+
245
+ assert buffer is not None
246
+ weights: list[tuple[str, torch.Tensor]] = []
247
+ for item in flatten_metas:
248
+ shape = item["shape"]
249
+ if isinstance(shape, list | tuple):
250
+ shape = torch.Size(shape)
251
+ assert isinstance(shape, torch.Size)
252
+ dtype, offset = item["dtype"], item["offset"]
253
+ size = dtype.itemsize * shape.numel()
254
+ tensor = buffer[offset : offset + size].view(dtype=dtype).view(shape)
255
+ weights.append((item["name"], tensor))
256
+ return weights
257
+
258
+
259
+ class CheckpointEngine:
260
+ """
261
+ CheckpointEngine class for control parameters synchronization.
262
+ Each trainer/rollout rank has a CheckpointEngine instance.
263
+ """
264
+
265
+ def __init__(
266
+ self, current_rank: int, actor_ranks: list[int], rollout_ranks: list[int], device_buffer_size_M: int
267
+ ) -> None:
268
+ self.current_rank = current_rank
269
+ self.actor_ranks = actor_ranks
270
+ self.rollout_ranks = rollout_ranks
271
+ # global_buckets saves the global MemoryBufferMeta infos.
272
+ # Thus each CheckpointEngine instance can control their operations in SPMD
273
+ self.global_buckets: dict[int, list[MemoryBufferMeta]] = None
274
+ # min device_buffer_size for h2d and broadcast
275
+ self.device_buffer_size_M = device_buffer_size_M
276
+
277
+ # ipc config for broadcast in pipeline mode
278
+ self._zmq_ctx = zmq.Context()
279
+ self._zmq_addr_counter: int = 0
280
+ device_index = self.current_rank % get_torch_device().device_count()
281
+ self._device_uuid = _get_physical_device_id(device_index)
282
+
283
+ def register_checkpoint(
284
+ self, weights_info: list[tuple[str, torch.Size, torch.dtype]], cpu_named_params: dict[str, torch.Tensor]
285
+ ):
286
+ """
287
+ Register checkpoint information and prepare memory buffers for parameter synchronization.
288
+
289
+ This function organizes the parameters into memory buckets for efficient synchronization
290
+ and prepares pinned memory buffers for faster data transfer between CPU and device.
291
+
292
+ Args:
293
+ weights_info (list[tuple[str, torch.Size, torch.dtype]]):
294
+ A list of tuples containing parameter name, shape, and data type.
295
+ cpu_named_params (dict[str, torch.Tensor]):
296
+ A dictionary mapping parameter names to their corresponding CPU tensors.
297
+
298
+ Steps:
299
+ 1. Calculate the bucket size based on the largest parameter tensor size and the device buffer size.
300
+ 2. Organize parameters into global buckets for each actor rank, ensuring that the total size of each bucket
301
+ does not exceed the bucket size.
302
+ 3. For actor ranks, allocate pinned memory buffers for each bucket and copy the parameter tensors
303
+ into these buffers.
304
+
305
+ Notes:
306
+ Each CheckpointEngine instance maintains the global buckets metas,
307
+ but stores part of parmas data in host memory
308
+ """
309
+ bucket_size = max(
310
+ self.device_buffer_size_M << 20, max(_align_size(dtype, shape) for _, shape, dtype in weights_info)
311
+ )
312
+ print(
313
+ f"set checkpoint_engine device buffer size: {self.device_buffer_size_M}M, "
314
+ f"and finally set it to {bucket_size >> 20}M considering the largest parameter tensor size"
315
+ )
316
+ self.bucket_size = bucket_size
317
+
318
+ # global_buckets saves the global MemoryBufferMeta infos.
319
+ if self.global_buckets is None:
320
+ self.global_buckets = {rank: [MemoryBufferMeta(size=0, metas=[])] for rank in self.actor_ranks}
321
+
322
+ actor_ranks_size = len(self.actor_ranks)
323
+ assert actor_ranks_size > 0, f"actor_ranks:{self.actor_ranks} should not be empty"
324
+ for param_idx, (param_name, param_shape, param_dtype) in enumerate(weights_info):
325
+ # Each parameter is assigned to an actor rank, and only this rank will store it
326
+ assgin_rank = self.actor_ranks[param_idx % actor_ranks_size]
327
+ param_size = _align_size(param_dtype, param_shape)
328
+
329
+ if self.global_buckets[assgin_rank][-1].size + param_size > bucket_size:
330
+ assert self.global_buckets[assgin_rank][-1].size, (
331
+ f"global_buckets[{assgin_rank}][-1].size:{self.global_buckets[assgin_rank][-1].size}"
332
+ " should not be 0"
333
+ )
334
+ self.global_buckets[assgin_rank].append(MemoryBufferMeta(size=0, metas=[]))
335
+ self.global_buckets[assgin_rank][-1].metas.append(
336
+ ParameterMeta(name=param_name, dtype=param_dtype, shape=param_shape)
337
+ )
338
+ self.global_buckets[assgin_rank][-1].size += param_size
339
+
340
+ def register_pin_memory(idx: int, size: int) -> tuple[int, torch.Tensor]:
341
+ """Allocate pinned memory for a bucket."""
342
+ buffer = torch.empty(size, dtype=torch.uint8, pin_memory=True)
343
+ return idx, buffer
344
+
345
+ def register_tensor(buffer: torch.Tensor, offset: int, tensor: torch.Tensor):
346
+ """Copy a tensor into a pinned memory buffer."""
347
+ buffer[offset : offset + tensor.nbytes] = tensor.view(-1).view(dtype=torch.uint8)
348
+
349
+ memory_buffers = [] # for rollout rank, return empty buffer
350
+ if self.current_rank in self.actor_ranks: # is_actor
351
+ local_buckets = self.global_buckets[self.current_rank]
352
+ memory_buffers = [
353
+ MemoryBuffer(buffer=torch.empty(0), size=bucket.size, metas=bucket.metas) for bucket in local_buckets
354
+ ]
355
+
356
+ # Use thread pool to accelerate organize parameters into buckets
357
+ with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
358
+ futures = [
359
+ executor.submit(register_pin_memory, idx, bucket.size) for idx, bucket in enumerate(local_buckets)
360
+ ]
361
+ new_futures = []
362
+ for future in concurrent.futures.as_completed(futures):
363
+ idx, buffer = future.result()
364
+ assert buffer.numel() == local_buckets[idx].size, (
365
+ f"buffer numel {buffer.numel()} should be equal to bucket size {local_buckets[idx].size}"
366
+ )
367
+ memory_buffers[idx].buffer = buffer
368
+ print(
369
+ f"[rank{self.current_rank}] register pin_memory for "
370
+ f" bucket {idx + 1}/{len(local_buckets)} finished, "
371
+ f"size {buffer.numel() / 1024 / 1024:.2f}MiB, start to copy tensors to buffer"
372
+ )
373
+ offset = 0
374
+ for meta in local_buckets[idx].metas:
375
+ name = meta.name
376
+ tensor = cpu_named_params[name]
377
+ size = _align_size(tensor.dtype, tensor.shape)
378
+ assert size == _align_size(meta.dtype, meta.shape), (
379
+ f"tensor {name} size {size} should be equal to "
380
+ f"meta size {_align_size(meta.dtype, meta.shape)}"
381
+ )
382
+ new_futures.append(executor.submit(register_tensor, buffer, offset, tensor))
383
+ offset += size
384
+ for future in concurrent.futures.as_completed(new_futures):
385
+ future.result()
386
+
387
+ self.memory_buffers = memory_buffers
388
+
389
+ def get_max_buckets_num_per_rank(self):
390
+ """
391
+ Get the maximum number of buckets for all rank.
392
+ """
393
+ assert self.global_buckets is not None
394
+ return max(len(buckets) for buckets in self.global_buckets.values())
395
+
396
+ def _bind_zmq_socket(self) -> tuple[zmq.Socket, list[tuple[str, str]]]:
397
+ """
398
+ Bind zmq socket for broadcast.
399
+ """
400
+
401
+ def zmq_handle(device_uuid: str) -> str:
402
+ return f"ipc://@checkpoint-engine-{device_uuid}-{self._zmq_addr_counter}.sock"
403
+
404
+ socket_path = zmq_handle(self._device_uuid)
405
+ socket = self._zmq_ctx.socket(zmq.REQ)
406
+ socket.bind(socket_path)
407
+ self._zmq_addr_counter += 1
408
+ return socket, socket_path
409
+
410
+ def update_checkpoint(self, inference_model, group_name: str, overlap_broadcast_and_consume: bool = False):
411
+ """
412
+ Update the checkpoint by broadcasting and loading weights.
413
+
414
+ This function handles the synchronization of parameters across ranks by:
415
+ 1. Copying data from memory buffers to device buffers (h2d_buffer).
416
+ 2. Broadcasting the data to all ranks using collective communication.
417
+ 3. Loading the weights into the inference model if provided.
418
+ 4. Optionally, use a pipeline approach for broadcasting and loading weights.
419
+
420
+ Args:
421
+ inference_model: The model to load weights into. If None (trainer rank), weights are only broadcasted.
422
+ group_name (str): The name of the collective communication group.
423
+ overlap_broadcast_and_consume (bool): Whether to use the pipeline approach
424
+ for broadcasting and loading weights.
425
+ """
426
+ try:
427
+ h2d_buffer: torch.Tensor | None = (
428
+ None
429
+ if self.current_rank in self.rollout_ranks
430
+ else torch.empty(self.bucket_size, dtype=torch.uint8, device=get_torch_device().current_device())
431
+ )
432
+ # for pipeline mode, we need to allocate 2x buffer size
433
+ broadcast_load_buffer = torch.empty(
434
+ self.bucket_size * (2 if overlap_broadcast_and_consume else 1),
435
+ dtype=torch.uint8,
436
+ device=get_torch_device().current_device(),
437
+ )
438
+ except Exception:
439
+ print(
440
+ "allocate buffer for update_checkpoint failed, "
441
+ "you may need to reduce "
442
+ "config.async_training.checkpoint_engine.device_buffer_size_M"
443
+ )
444
+ raise
445
+
446
+ max_h2d_iter = self.get_max_buckets_num_per_rank()
447
+
448
+ if overlap_broadcast_and_consume:
449
+ socket, socket_path = self._bind_zmq_socket()
450
+
451
+ # Define a function to update weights from IPC
452
+ def update_weights_from_ipc_(socket_path):
453
+ zmq_ctx = zmq.Context()
454
+ socket = zmq_ctx.socket(zmq.REP)
455
+ socket.connect(socket_path)
456
+ socket.recv_pyobj()
457
+ socket.send(b"")
458
+
459
+ while True:
460
+ payload: tuple[Callable, tuple] | list[FlattenedTensorMetadata] | None = socket.recv_pyobj()
461
+ if payload is None:
462
+ # means the update is done
463
+ get_torch_device().synchronize()
464
+ socket.send(b"")
465
+ break
466
+ assert isinstance(payload, list)
467
+ if inference_model is not None:
468
+ inference_model.load_weights(_extract_weights(payload, broadcast_load_buffer))
469
+ get_torch_device().synchronize()
470
+ socket.send(b"")
471
+
472
+ req_thread = threading.Thread(
473
+ target=update_weights_from_ipc_,
474
+ args=(socket_path,),
475
+ )
476
+ req_thread.start()
477
+ socket.send_pyobj(b"")
478
+ get_torch_device().synchronize()
479
+
480
+ gidx = 0
481
+ local_buckets = self.global_buckets.get(self.current_rank, [])
482
+
483
+ for i in range(max_h2d_iter):
484
+ # Step 1: Each actor rank copy the parameter tensor into device memory
485
+ if i < len(self.memory_buffers):
486
+ h2d_buffer[: local_buckets[i].size].data.copy_(self.memory_buffers[i].buffer)
487
+
488
+ # Step 2: Broadcast the device data in turn
489
+ for broadcast_rank, _buckets in self.global_buckets.items():
490
+ if i >= len(_buckets):
491
+ continue
492
+ bucket = _buckets[i]
493
+
494
+ # Prepare the broadcast buffer
495
+ start = gidx % 2 * self.bucket_size if overlap_broadcast_and_consume else 0
496
+ buffer_b: torch.Tensor = broadcast_load_buffer[start : start + bucket.size]
497
+ if broadcast_rank == self.current_rank:
498
+ buffer_b.data.copy_(h2d_buffer[: bucket.size])
499
+
500
+ # Broadcast the buffer to all ranks
501
+ collective.broadcast(buffer_b, src_rank=broadcast_rank, group_name=group_name)
502
+
503
+ if overlap_broadcast_and_consume:
504
+ socket.recv()
505
+ collective.barrier(group_name=group_name)
506
+ socket.send_pyobj(_to_flattened_tensor_meta(bucket.metas, start))
507
+ elif inference_model is not None:
508
+ named_tensor = _to_flattened_tensor_meta(bucket.metas, 0)
509
+ inference_model.load_weights(_extract_weights(named_tensor, buffer_b))
510
+
511
+ gidx += 1
512
+
513
+ if overlap_broadcast_and_consume:
514
+ socket.recv()
515
+ socket.send_pyobj(None)
516
+ socket.recv()
517
+ req_thread.join()
518
+ socket.close()
519
+
520
+ collective.barrier(group_name=group_name)
521
+ # clear host memory cache
522
+ self.memory_buffers = []
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_megatron_trainer.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hydra:
2
+ searchpath:
3
+ - file://verl/trainer/config
4
+
5
+ defaults:
6
+ - ppo_megatron_trainer
7
+ - _self_
8
+
9
+ async_training:
10
+
11
+ # Maximum samples staleness threshold
12
+ staleness_threshold: 0.1
13
+
14
+ # Frequency of parameter synchronization between rollouter and trainer,
15
+ # One step means trainer obtains a batch of required samples
16
+ trigger_parameter_sync_step: 4
17
+
18
+ # The number of ppo_mini_batches that the FullyAsyncTrainer obtains once
19
+ require_batches: 1
20
+
21
+ # When synchronizing parameters, whether to interrupt rollouter and perform partial rollout
22
+ partial_rollout: True
23
+
24
+ # Whether to use rollout log probs for training
25
+ use_rollout_log_probs: True
26
+
27
+ # compute_prox_log_prob
28
+ compute_prox_log_prob: False
29
+
30
+ # whether to use trainer do_validate
31
+ use_trainer_do_validate: False
32
+
33
+
34
+ # checkpoint_engine config for accelerating parameter synchronization between rollouter and trainer
35
+ checkpoint_engine:
36
+ # Whether to use checkpoint_engine
37
+ enable: True
38
+
39
+ # Device buffer size for checkpoint_engine, default is 4096 MB
40
+ device_buffer_size_M: 4096
41
+
42
+ # Enable the pipeline for broadcasting and updating parameters, but it requires more device memory
43
+ overlap_broadcast_and_consume: False
44
+
45
+ # Rollout config
46
+ rollout:
47
+
48
+ # Number of nodes used in the rollout
49
+ nnodes: 1
50
+
51
+ # Number of GPUs per node
52
+ n_gpus_per_node: 8
53
+
54
+ # number of responses (i.e. num sample times). > 1 for grpo
55
+ n: 4
56
+
57
+ # total rollout samples # TODO rename to total_rollout_samples
58
+ total_rollout_steps: 100
59
+
60
+ # Number of epochs in training
61
+ total_epochs: 10
62
+
63
+ # Test frequency, how many times a parameter update triggers a validation
64
+ test_freq: 1
65
+
66
+ data:
67
+ # Number of samples generated, currently only support 1
68
+ gen_batch_size: 1
69
+
70
+ actor_rollout_ref:
71
+ # checkpoint_engine config for accelerating parameter synchronization between rollouter and trainer
72
+ checkpoint_engine: ${oc.select:async_training.checkpoint_engine, null}
73
+
74
+ actor:
75
+ # Whether to use rollout log probs for training
76
+ use_rollout_log_probs: ${oc.select:async_training.use_rollout_log_probs, True}
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_trainer.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hydra:
2
+ searchpath:
3
+ - file://verl/trainer/config
4
+
5
+ defaults:
6
+ - ppo_trainer
7
+ - _self_
8
+
9
+ async_training:
10
+
11
+ # Maximum samples staleness threshold
12
+ staleness_threshold: 0.1
13
+
14
+ # Frequency of parameter synchronization between rollouter and trainer,
15
+ # One step means trainer obtains a batch of required samples
16
+ trigger_parameter_sync_step: 4
17
+
18
+ # The number of ppo_mini_batches that the FullyAsyncTrainer obtains once
19
+ require_batches: 1
20
+
21
+ # When synchronizing parameters, whether to interrupt rollouter and perform partial rollout
22
+ partial_rollout: True
23
+
24
+ # Whether to use rollout log probs for training
25
+ use_rollout_log_probs: True
26
+
27
+ # compute_prox_log_prob
28
+ compute_prox_log_prob: False
29
+
30
+ # whether to use trainer do_validate
31
+ use_trainer_do_validate: False
32
+
33
+
34
+ # checkpoint_engine config for accelerating parameter synchronization between rollouter and trainer
35
+ checkpoint_engine:
36
+ # Whether to use checkpoint_engine
37
+ enable: True
38
+
39
+ # Device buffer size for checkpoint_engine, default is 4096 MB
40
+ device_buffer_size_M: 4096
41
+
42
+ # Enable the pipeline for broadcasting and updating parameters, but it requires more device memory
43
+ overlap_broadcast_and_consume: False
44
+
45
+ # Rollout config
46
+ rollout:
47
+
48
+ # Number of nodes used in the rollout
49
+ nnodes: 1
50
+
51
+ # Number of GPUs per node
52
+ n_gpus_per_node: 8
53
+
54
+ # number of responses (i.e. num sample times). > 1 for grpo
55
+ n: 4
56
+
57
+ # total rollout samples # TODO rename to total_rollout_samples
58
+ total_rollout_steps: 100
59
+
60
+ # Number of epochs in training
61
+ total_epochs: 10
62
+
63
+ # Test frequency, how many times a parameter update triggers a validation
64
+ test_freq: 1
65
+
66
+ data:
67
+ # Number of samples generated, currently only support 1
68
+ gen_batch_size: 1
69
+
70
+ actor_rollout_ref:
71
+ # checkpoint_engine config for accelerating parameter synchronization between rollouter and trainer
72
+ checkpoint_engine: ${oc.select:async_training.checkpoint_engine, null}
73
+
74
+ actor:
75
+ # Whether to use rollout log probs for training
76
+ use_rollout_log_probs: ${oc.select:async_training.use_rollout_log_probs, True}
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/detach_utils.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import time
15
+ from collections import defaultdict
16
+ from dataclasses import dataclass
17
+ from typing import Any, Optional
18
+
19
+ import numpy as np
20
+ import torch
21
+
22
+ from verl import DataProto
23
+ from verl.experimental.agent_loop.agent_loop import AgentLoopOutput
24
+ from verl.trainer.ppo.ray_trainer import compute_response_mask
25
+
26
+
27
+ @dataclass
28
+ class RolloutSample:
29
+ """Enhanced rollout sample containing both original batch info and AgentLoopOutput"""
30
+
31
+ # Original batch information
32
+ full_batch: Any
33
+
34
+ # AgentLoopOutput from generation
35
+ agent_loop_output_list: list[AgentLoopOutput]
36
+
37
+ # Metadata
38
+ sample_id: str
39
+ epoch: int
40
+
41
+ # Processing metadata
42
+ processing_times: list[float]
43
+ tool_calls: list[float]
44
+ param_version: int
45
+ param_version_start: list[int]
46
+ param_version_end: list[int]
47
+ rollout_status: dict[str, Any]
48
+
49
+
50
+ @dataclass
51
+ class ValidateMetrics:
52
+ """Metrics for validation"""
53
+
54
+ timing_raw: dict[str, Any]
55
+ metrics: Optional[dict[str, Any]] = None
56
+ global_steps: Optional[int] = None
57
+ param_version: Optional[int] = None
58
+
59
+
60
+ def prepare_single_generation_data(batch_dict, config) -> DataProto:
61
+ """
62
+ Similar to the logic of ray_trainer._prepare_generate_batch, but for a single sample.
63
+ Separate the data used for generation from the original data.
64
+
65
+ Returns:
66
+ tuple: (original_batch_dict, gen_data_for_single_sample)
67
+ """
68
+
69
+ full_batch = DataProto.from_single_dict(batch_dict)
70
+
71
+ batch_keys_to_pop = []
72
+ non_tensor_batch_keys_to_pop = []
73
+
74
+ existing_batch_keys = [k for k in batch_keys_to_pop if k in full_batch.batch.keys()]
75
+ existing_non_tensor_keys = [k for k in non_tensor_batch_keys_to_pop if k in full_batch.non_tensor_batch.keys()]
76
+
77
+ if existing_batch_keys or existing_non_tensor_keys:
78
+ full_batch.pop(
79
+ batch_keys=existing_batch_keys,
80
+ non_tensor_batch_keys=existing_non_tensor_keys,
81
+ )
82
+
83
+ # Setting selected agent, that supports partial
84
+ if config.actor_rollout_ref.rollout.multi_turn.enable:
85
+ full_batch.non_tensor_batch["agent_name"] = np.array(
86
+ ["async_partial_tool_agent"] * len(full_batch), dtype=object
87
+ )
88
+ else:
89
+ full_batch.non_tensor_batch["agent_name"] = np.array(
90
+ ["partial_single_turn_agent"] * len(full_batch), dtype=object
91
+ )
92
+
93
+ # Add global step count to generated data
94
+ full_batch = full_batch.repeat(repeat_times=config.actor_rollout_ref.rollout.n, interleave=True)
95
+ return full_batch
96
+
97
+
98
+ def assemble_batch_from_rollout_samples(
99
+ rollout_samples: list[RolloutSample], tokenizer, config, balance_batch=None
100
+ ) -> DataProto:
101
+ """
102
+ Assemble gen_batch_output from RolloutSample objects
103
+ Assembles batches from RolloutSample objects, similar to the _post_generate_batch logic in ray_trainer.
104
+
105
+ Args:
106
+ rollout_samples: List of RolloutSample objects
107
+ tokenizer: Tokenizer instance
108
+ config: Configuration object containing trainer settings
109
+ balance_batch: Whether to balance the batch (simplified version)
110
+
111
+ Returns:
112
+ DataProto: Assembled gen_batch_output
113
+
114
+ Raises:
115
+ ValueError: If rollout_samples is empty
116
+ """
117
+ start_time = time.time()
118
+
119
+ if not rollout_samples:
120
+ raise ValueError("Empty rollout_samples provided for batch assembly")
121
+
122
+ print(f"[BatchUtils] Assembling batch from {len(rollout_samples)} RolloutSample objects")
123
+
124
+ rollout_samples_batch = []
125
+ processing_times = []
126
+ tool_calls = []
127
+ rollout_status = rollout_samples[0].rollout_status
128
+ # Add a prefix to all rollout_status keys
129
+ rollout_status = {f"fully_async/{key}": value for key, value in rollout_status.items()}
130
+
131
+ for rs in rollout_samples:
132
+ rollout_samples_batch.append(rs.full_batch)
133
+ final_batch = DataProto.concat(rollout_samples_batch)
134
+
135
+ # Calculate response_mask (if not present)
136
+ if "response_mask" not in final_batch.batch.keys():
137
+ final_batch.batch["response_mask"] = compute_response_mask(final_batch)
138
+
139
+ if balance_batch:
140
+ balance_batch(final_batch, metrics={})
141
+
142
+ # Calculate the global valid token number
143
+ if "attention_mask" in final_batch.batch:
144
+ final_batch.meta_info["global_token_num"] = torch.sum(final_batch.batch["attention_mask"], dim=-1).tolist()
145
+
146
+ processing_times = final_batch.non_tensor_batch["processing_times"]
147
+ tool_calls = final_batch.non_tensor_batch["tool_calls_times"]
148
+ # Collect statistics
149
+
150
+ processing_time_stats = {
151
+ "processing_time/avg": np.mean(processing_times),
152
+ "processing_time/max": np.max(processing_times),
153
+ "processing_time/min": np.min(processing_times),
154
+ "processing_time/tp50": np.percentile(processing_times, 50),
155
+ "processing_time/tp99": np.percentile(processing_times, 99),
156
+ "processing_time/tp95": np.percentile(processing_times, 95),
157
+ }
158
+ tool_calls_stats = {}
159
+ if len(tool_calls) > 0:
160
+ tool_calls_stats = {
161
+ "timing_s/agent_loop/tool_calls/max": np.max(tool_calls),
162
+ "timing_s/agent_loop/tool_calls/min": np.min(tool_calls),
163
+ "timing_s/agent_loop/tool_calls/mean": np.mean(tool_calls),
164
+ }
165
+ processing_time_stats = {f"fully_async/{key}": value for key, value in processing_time_stats.items()}
166
+
167
+ param_version_start = final_batch.non_tensor_batch["param_version_start"]
168
+ param_version_end = final_batch.non_tensor_batch["param_version_end"]
169
+ param_version_diff = [abs(a - b) for a, b in zip(param_version_end, param_version_start, strict=False)]
170
+ num_diff0 = param_version_diff.count(0)
171
+ partial_stats = {
172
+ "fully_async/partial/total_partial_num": len(param_version_diff) - num_diff0,
173
+ "fully_async/partial/partial_ratio": (len(param_version_diff) - num_diff0) / len(param_version_diff),
174
+ "fully_async/partial/max_partial_span": max(param_version_diff),
175
+ }
176
+ # add meta_info
177
+ param_versions = [rs.param_version for rs in rollout_samples]
178
+ trajectorys_param_versions = final_batch.non_tensor_batch["param_version_end"]
179
+
180
+ final_batch.meta_info.update(
181
+ {
182
+ "rollout_param_versions": param_versions,
183
+ "param_version_diversity": len(set(param_versions)) if param_versions else 0,
184
+ "trajectory_param_versions": trajectorys_param_versions,
185
+ **processing_time_stats,
186
+ **rollout_status,
187
+ **partial_stats,
188
+ **tool_calls_stats,
189
+ }
190
+ )
191
+
192
+ print(f"[BatchUtils] Batch assembly completed in {time.time() - start_time:.2f}s")
193
+
194
+ return final_batch
195
+
196
+
197
+ class MetricsAggregator:
198
+ """Metrics aggregator, used to combine metrics from multiple training steps"""
199
+
200
+ def __init__(self, total_gpus: int):
201
+ # Store all values ​​for each metric
202
+ self.metric_values: dict[str, list[float]] = defaultdict(list)
203
+ # Store the number of samples at each step for weighted averaging
204
+ self.sample_counts: list[int] = []
205
+ # Store the timestamp of each step for time-related calculations
206
+ self.timestamps: list[float] = []
207
+ # Step Count
208
+ self.step_count = 0
209
+ # total num gpus used
210
+ self.total_gpus = total_gpus
211
+
212
+ # Metric aggregation rule configuration
213
+ self.aggregation_rules = self._init_aggregation_rules()
214
+
215
+ def _init_aggregation_rules(self) -> dict[str, dict[str, list[str]]]:
216
+ """Initialize metrics aggregation rules"""
217
+ return {
218
+ # Time-Based metrics, can add metrics here
219
+ "time_sum": ["perf/time_per_step"],
220
+ "min": ["timing_s/agent_loop/tool_calls/min"],
221
+ "avg": ["timing_s/agent_loop/tool_calls/mean"],
222
+ "max": ["timing_s/agent_loop/tool_calls/max"],
223
+ "last": [
224
+ "fully_async/count/total_generated_samples",
225
+ "fully_async/count/stale_samples_processed",
226
+ "fully_async/count/stale_trajectory_processed",
227
+ "fully_async/count/current_param_version",
228
+ "fully_async/count/dropped_stale_samples",
229
+ "training/global_step", # TODO change name to: total_step
230
+ ],
231
+ }
232
+
233
+ def add_step_metrics(self, metrics: dict[str, Any], sample_count: int, timestamp: float = None):
234
+ """Adding a single-step metrics"""
235
+ if timestamp is None:
236
+ timestamp = time.time()
237
+
238
+ self.sample_counts.append(sample_count)
239
+ self.timestamps.append(timestamp)
240
+ self.step_count += 1
241
+
242
+ # Store all metrics values
243
+ for key, value in metrics.items():
244
+ if isinstance(value, int | float | np.number):
245
+ self.metric_values[key].append(float(value))
246
+ elif isinstance(value, torch.Tensor):
247
+ self.metric_values[key].append(float(value.item()))
248
+
249
+ def _get_aggregation_type(self, metric_name: str) -> str:
250
+ """Determine the aggregation type based on the metric name"""
251
+ for agg_type, metric_list in self.aggregation_rules.items():
252
+ if metric_name in metric_list:
253
+ return agg_type
254
+
255
+ metric_lower = metric_name.lower()
256
+ if any(keyword in metric_lower for keyword in ["timing_s/"]):
257
+ return "time_sum"
258
+ if any(keyword in metric_lower for keyword in ["mean", "avg", "average"]):
259
+ return "avg"
260
+ if any(keyword in metric_lower for keyword in ["max", "maximum"]):
261
+ return "max"
262
+ if any(keyword in metric_lower for keyword in ["min", "minimum"]):
263
+ return "min"
264
+ if any(keyword in metric_lower for keyword in ["sum", "total"]):
265
+ return "sum"
266
+ if any(keyword in metric_lower for keyword in ["weighted_avg"]):
267
+ return "weighted_avg"
268
+
269
+ return "avg"
270
+
271
+ def _aggregate_single_metric(self, metric_name: str, values: list[float]) -> float:
272
+ """Aggregating a single metric"""
273
+ if not values:
274
+ return 0.0
275
+
276
+ agg_type = self._get_aggregation_type(metric_name)
277
+
278
+ if agg_type == "last":
279
+ return values[-1]
280
+
281
+ elif agg_type == "weighted_avg":
282
+ # Weighted average
283
+ if len(values) != len(self.sample_counts):
284
+ # If the lengths do not match, use a simple average
285
+ return sum(values) / len(values)
286
+
287
+ total_samples = sum(self.sample_counts)
288
+ if total_samples == 0:
289
+ return sum(values) / len(values)
290
+
291
+ weighted_sum = sum(v * c for v, c in zip(values, self.sample_counts, strict=False))
292
+ return weighted_sum / total_samples
293
+
294
+ elif agg_type == "sum" or agg_type == "time_sum":
295
+ return sum(values)
296
+
297
+ elif agg_type == "avg":
298
+ return sum(values) / len(values)
299
+
300
+ elif agg_type == "max":
301
+ return max(values)
302
+
303
+ elif agg_type == "min":
304
+ return min(values)
305
+
306
+ else:
307
+ # Default average
308
+ return sum(values) / len(values)
309
+
310
+ def get_aggregated_metrics(self) -> dict[str, Any]:
311
+ """aggregated metrics"""
312
+ t = time.time()
313
+ if self.step_count == 0:
314
+ return {}
315
+
316
+ aggregated = {}
317
+
318
+ # Aggregate all metrics
319
+ for metric_name, values in self.metric_values.items():
320
+ aggregated[metric_name] = self._aggregate_single_metric(metric_name, values)
321
+
322
+ # Aggregate special metrics
323
+ aggregated = self._special_metrics_aggergate(aggregated)
324
+
325
+ print(f"aggregated metrics done. cost {time.time() - t}")
326
+
327
+ return aggregated
328
+
329
+ def _special_metrics_aggergate(self, aggregated: dict[str, Any]) -> dict[str, Any]:
330
+ """calculate special metrics"""
331
+
332
+ # global_seqlen/minmax_diff
333
+ if "global_seqlen/minmax_diff" in aggregated.keys():
334
+ aggregated["global_seqlen/minmax_diff"] = aggregated["global_seqlen/max"] - aggregated["global_seqlen/min"]
335
+
336
+ # perf/throughput
337
+ REQUIRED_PERF_KEYS = {"perf/throughput", "perf/total_num_tokens", "perf/time_per_step"}
338
+ if REQUIRED_PERF_KEYS.issubset(aggregated):
339
+ aggregated["perf/throughput"] = aggregated["perf/total_num_tokens"] / (
340
+ aggregated["perf/time_per_step"] * self.total_gpus
341
+ )
342
+
343
+ # trainer/idle_ratio
344
+ if "timing_s/gen" in aggregated.keys() and "timing_s/step" in aggregated.keys():
345
+ aggregated["trainer/idle_ratio"] = aggregated["timing_s/gen"] / aggregated["timing_s/step"]
346
+
347
+ return aggregated
348
+
349
+ def reset(self):
350
+ """Reset Aggregator"""
351
+ self.metric_values.clear()
352
+ self.sample_counts.clear()
353
+ self.timestamps.clear()
354
+ self.step_count = 0
355
+
356
+ def get_current_stats(self) -> dict[str, Any]:
357
+ """Get statistics about the current aggregation state (for debugging)"""
358
+ return {
359
+ "step_count": self.step_count,
360
+ "metric_count": len(self.metric_values),
361
+ "total_samples": sum(self.sample_counts),
362
+ "metric_names": list(self.metric_values.keys()),
363
+ }
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp2_utils.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from typing import Optional
17
+
18
+ import torch
19
+ import torch.distributed as dist
20
+ from packaging import version
21
+ from torch.distributed.tensor import DTensor
22
+ from torch.distributed.tensor._dtensor_spec import DTensorSpec
23
+
24
+ if version.parse(torch.__version__) < version.parse("2.6"):
25
+ raise RuntimeError("PyTorch 2.6 or higher is required to use fstp_utils.")
26
+
27
+
28
+ def fsdp2_sharded_save_to_cpu(
29
+ model: torch.nn.Module,
30
+ ) -> tuple[dict[str, tuple[torch.Tensor, DTensorSpec]], DTensorSpec]:
31
+ """
32
+ Sharded Save: Each process only saves the local DTensor shard from its own GPU to CPU memory.
33
+
34
+ Args:
35
+ model: FSDP2-wrapped model whose parameters are of DTensor type.
36
+
37
+ Returns:
38
+ cpu_sharded_state: Dictionary of CPU shards for the current process.
39
+ Key = parameter name, Value = (CPU shard tensor, original DTensorSpec)
40
+ global_spec: DTensorSpec of the first parameter (used to verify global rules during loading)
41
+ """
42
+ cpu_sharded_state = {}
43
+ global_spec = None # Record global sharding rules (all parameters follow the same spec)
44
+
45
+ for param_name, param in model.named_parameters():
46
+ # Only process sharded parameters of DTensor type (core parameters of FSDP2)
47
+ if not isinstance(param, DTensor):
48
+ # Save non-sharded parameters (e.g., running_mean of BatchNorm) as local data
49
+ cpu_tensor = param.detach().cpu()
50
+ cpu_sharded_state[param_name] = (cpu_tensor, None)
51
+ continue
52
+
53
+ # Record global sharding rules (take spec of the first DTensor to ensure consistency)
54
+ if global_spec is None:
55
+ global_spec = param._spec
56
+ assert hasattr(global_spec, "device_mesh"), "DTensorSpec must contain 'device_mesh' attribute"
57
+ assert hasattr(global_spec, "placements"), "DTensorSpec must contain 'placements' attribute"
58
+
59
+ # 1. Extract local shard data from the current GPU (_local_tensor)
60
+ local_gpu_tensor = param._local_tensor # Local shard attribute defined in your DTensor class
61
+ # 2. Move to CPU memory and detach from computation graph
62
+ local_cpu_tensor = local_gpu_tensor.detach().cpu()
63
+ # 3. Save CPU shard + original DTensorSpec (ensure sharding rules remain unchanged)
64
+ cpu_sharded_state[param_name] = (local_cpu_tensor, param._spec)
65
+
66
+ assert global_spec is not None, "No DTensor-type parameters found in the model. FSDP2 sharding may not be enabled."
67
+ return cpu_sharded_state, global_spec
68
+
69
+
70
+ def fsdp2_sharded_load_from_cpu(
71
+ model: torch.nn.Module,
72
+ cpu_sharded_state: dict[str, tuple[torch.Tensor, Optional[DTensorSpec]]],
73
+ target_spec: DTensorSpec,
74
+ ) -> None:
75
+ """
76
+ Sharded Load: Each process only loads the CPU shard it is responsible for to the GPU,
77
+ keeping sharding rules unchanged.
78
+
79
+ Args:
80
+ model: FSDP2 model to be restored (must have the same structure as when saved)
81
+ cpu_sharded_state: Shard data read from CPU memory by the current process
82
+ (from fsdp2_sharded_save_to_cpu)
83
+ target_spec: Global DTensorSpec from saving (used to verify sharding rule consistency)
84
+ """
85
+ # Verify device_mesh consistency (core: ensure loaded shards map to original GPUs)
86
+ current_device_mesh = None
87
+ for param in model.parameters():
88
+ if isinstance(param, DTensor):
89
+ current_device_mesh = param._spec.device_mesh
90
+ break
91
+ assert current_device_mesh is not None, "DTensor parameters not initialized in the model to be loaded"
92
+ assert current_device_mesh == target_spec.device_mesh, (
93
+ f"device_mesh mismatch during loading! Original: {target_spec.device_mesh}, Current: {current_device_mesh}"
94
+ )
95
+
96
+ for param_name, param in model.named_parameters():
97
+ # Skip parameters not in the saved state (e.g., newly added parameters)
98
+ if param_name not in cpu_sharded_state:
99
+ continue
100
+
101
+ # Extract CPU shard data and original Spec
102
+ local_cpu_tensor, saved_spec = cpu_sharded_state[param_name]
103
+
104
+ # Handle different parameter types: DTensor sharded parameters vs. regular parameters
105
+ if isinstance(param, DTensor):
106
+ # 1. Verify sharding rule consistency (placements must match original Spec)
107
+ assert saved_spec is not None, f"DTensorSpec missing in saved state for parameter {param_name}"
108
+ assert saved_spec.placements == target_spec.placements, (
109
+ f"Sharding strategy mismatch for parameter {param_name} (conflicts with global rules)!"
110
+ )
111
+
112
+ # 2. Move CPU shard data to the current GPU (device of param._local_tensor)
113
+ target_device = param._local_tensor.device
114
+ local_gpu_tensor = local_cpu_tensor.to(target_device)
115
+
116
+ # 3. Restore to DTensor's local shard (directly copy to _local_tensor, keep spec unchanged)
117
+ param._local_tensor.copy_(local_gpu_tensor)
118
+
119
+ else:
120
+ # Regular parameters: load directly to original device
121
+ target_device = param.device
122
+ param.data.copy_(local_cpu_tensor.to(target_device))
123
+
124
+ # Process synchronization: ensure all processes complete loading before proceeding
125
+ dist.barrier()
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp_workers.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ import os
18
+ import time
19
+
20
+ import torch
21
+ import torch.distributed
22
+ from omegaconf import DictConfig
23
+ from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
24
+
25
+ from verl.experimental.fully_async_policy.base_detach_sync import BaseDetachNcclSync
26
+ from verl.experimental.fully_async_policy.fsdp2_utils import fsdp2_sharded_load_from_cpu, fsdp2_sharded_save_to_cpu
27
+ from verl.single_controller.base.decorator import Dispatch, register
28
+ from verl.utils.device import (
29
+ get_device_name,
30
+ get_torch_device,
31
+ )
32
+ from verl.utils.fsdp_utils import (
33
+ fsdp_version,
34
+ load_fsdp_model_to_gpu,
35
+ offload_fsdp_model_to_cpu,
36
+ )
37
+ from verl.workers.fsdp_workers import AsyncActorRolloutRefWorker, CriticWorker
38
+
39
+ logger = logging.getLogger(__file__)
40
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
41
+
42
+ device_name = get_device_name()
43
+
44
+ __all__ = ["DetachActorWorker", "DetachAsyncRolloutWorker", "CriticWorker"]
45
+
46
+
47
+ class DetachNcclSync(BaseDetachNcclSync, AsyncActorRolloutRefWorker):
48
+ def __init__(self, config: DictConfig, role: str):
49
+ BaseDetachNcclSync.__init__(self, config, role)
50
+ AsyncActorRolloutRefWorker.__init__(self, config, role)
51
+
52
+ def _get_actor_params(self):
53
+ pass
54
+
55
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
56
+ def sync_rollout_weights(self, sync_group_name="actor_rollout"):
57
+ assert (self._is_actor or self._is_rollout) and not self.config.hybrid_engine
58
+ assert hasattr(self, "_weights_info") and self._weights_info is not None
59
+
60
+ if self._is_actor and self._is_offload_param:
61
+ load_fsdp_model_to_gpu(self.actor_module_fsdp)
62
+ params = self._get_actor_params() if self._is_actor else None
63
+ rollout_name = self.config.rollout.name
64
+
65
+ inference_model = None
66
+ if self._is_rollout:
67
+ if rollout_name == "vllm":
68
+ inference_model = BaseDetachNcclSync.get_inference_model(self.rollout)
69
+
70
+ from verl.utils.vllm.patch import patch_vllm_moe_model_weight_loader
71
+
72
+ patch_vllm_moe_model_weight_loader(inference_model)
73
+ elif rollout_name == "sglang":
74
+ inference_model = self.rollout._engine
75
+ # For ServerAdapter, _engine might be None and needs async initialization
76
+ if inference_model is None:
77
+ # Initialize the server adapter engine
78
+ print("[sync_rollout_weights] Initialize server adapter engine")
79
+
80
+ async def init_engine():
81
+ if hasattr(self.rollout, "_init_server_adapter"):
82
+ await self.rollout._init_server_adapter()
83
+ else:
84
+ print("[sync_rollout_weights] No _init_server_adapter method found")
85
+ return self.rollout._engine
86
+
87
+ inference_model = self._run_async_safely(init_engine())
88
+ if inference_model is None:
89
+ raise RuntimeError(
90
+ f"Failed to initialize rollout engine. "
91
+ f"rollout type: {type(self.rollout)}, "
92
+ f"has _init_server_adapter: {hasattr(self.rollout, '_init_server_adapter')}"
93
+ )
94
+ else:
95
+ raise NotImplementedError(f"Unknown rollout name: {rollout_name}")
96
+
97
+ if rollout_name == "sglang" and self._is_rollout:
98
+ self._sync_sglang_weights(inference_model, params, sync_group_name)
99
+ else:
100
+ self._sync_vllm_weights(inference_model, params, sync_group_name)
101
+
102
+ if self._is_actor and self._is_offload_param:
103
+ offload_fsdp_model_to_cpu(self.actor_module_fsdp)
104
+ get_torch_device().empty_cache()
105
+
106
+ def cache_actor_weights_to_cpu(self):
107
+ self.cpu_named_params = {}
108
+ if self._is_actor:
109
+ params = self._get_actor_params()
110
+ local_rank = torch.distributed.get_rank()
111
+ world_size = torch.distributed.get_world_size()
112
+
113
+ for tensor_idx, (key, _, _) in enumerate(self._weights_info):
114
+ origin_data = params[key]
115
+ if hasattr(origin_data, "full_tensor"):
116
+ origin_data = origin_data.full_tensor()
117
+
118
+ if tensor_idx % world_size == local_rank:
119
+ self.cpu_named_params[key] = origin_data.to("cpu", non_blocking=True)
120
+ get_torch_device().synchronize()
121
+
122
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
123
+ def sync_rollout_weights_by_checkpoint(self, sync_group_name="actor_rollout"):
124
+ assert (self._is_actor or self._is_rollout) and not self.config.hybrid_engine
125
+ assert hasattr(self, "_weights_info") and self._weights_info is not None
126
+
127
+ # Load model to GPU
128
+ load_start_time = time.time()
129
+ if self._is_actor and self._is_offload_param:
130
+ load_fsdp_model_to_gpu(self.actor_module_fsdp)
131
+ load_duration = time.time() - load_start_time
132
+
133
+ from ray.util.collective import collective
134
+
135
+ # Cache actor weights to CPU and measure the time taken
136
+ cache_start_time = time.time()
137
+ self.cache_actor_weights_to_cpu()
138
+ cache_end_time = time.time()
139
+ cache_duration = cache_end_time - cache_start_time
140
+
141
+ # Register the cached weights into the checkpoint engine
142
+ self.checkpoint_engine.register_checkpoint(self._weights_info, self.cpu_named_params)
143
+ register_end_time = time.time()
144
+ register_duration = register_end_time - cache_end_time
145
+ self.cpu_named_params = {}
146
+
147
+ collective.barrier(group_name=sync_group_name)
148
+ update_start_time = time.time()
149
+
150
+ inference_model = None
151
+ if self._is_rollout:
152
+ inference_model = BaseDetachNcclSync.get_inference_model(self.rollout)
153
+ from verl.utils.vllm.patch import patch_vllm_moe_model_weight_loader
154
+
155
+ patch_vllm_moe_model_weight_loader(inference_model)
156
+
157
+ # Update the checkpoint with the inference model and broadcast weights
158
+ self.checkpoint_engine.update_checkpoint(
159
+ inference_model=inference_model,
160
+ group_name=sync_group_name,
161
+ overlap_broadcast_and_consume=self.config.checkpoint_engine.overlap_broadcast_and_consume,
162
+ )
163
+
164
+ update_end_time = time.time()
165
+ update_duration = update_end_time - update_start_time
166
+
167
+ offload_start_time = time.time()
168
+ if self._is_actor and self._is_offload_param:
169
+ offload_fsdp_model_to_cpu(self.actor_module_fsdp)
170
+ offload_duration = time.time() - offload_start_time
171
+
172
+ print(
173
+ f"sync_rollout_weights_by_checkpoint finish!, rank:{torch.distributed.get_rank()},"
174
+ f" is_actor:{self._is_actor}, is_rollout:{self._is_rollout},"
175
+ f" total cost:{update_end_time - cache_start_time} seconds, while cache cost {cache_duration} seconds, "
176
+ f" register cost {register_duration} seconds, update cost {update_duration} seconds"
177
+ )
178
+
179
+ if self._is_actor and self._is_offload_param:
180
+ print(
181
+ f"sync_rollout_weights_by_checkpoint load model to gpu cost {load_duration} seconds,"
182
+ f" offload model to cpu cost {offload_duration} seconds"
183
+ )
184
+
185
+
186
+ class DetachActorWorker(DetachNcclSync):
187
+ def __init__(self, config: DictConfig, role: str):
188
+ print("[DetachAsyncRolloutWorker] Initializing via DetachNcclSync...")
189
+ DetachNcclSync.__init__(self, config, role)
190
+
191
+ def _get_actor_params(self):
192
+ assert self._is_actor
193
+ params = self.actor_module_fsdp.state_dict()
194
+ from verl.utils.model import convert_weight_keys
195
+
196
+ params = convert_weight_keys(
197
+ params, getattr(self.actor_module_fsdp, "_fsdp_wrapped_module", self.actor_module_fsdp)
198
+ )
199
+ return params
200
+
201
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
202
+ def get_actor_weights_info(self):
203
+ assert self._is_actor
204
+ if hasattr(self, "_weights_info"):
205
+ return self._weights_info
206
+ if fsdp_version(self.actor_module_fsdp) == 1:
207
+ from torch.distributed.fsdp.api import ShardedStateDictConfig, StateDictType
208
+
209
+ FSDP.set_state_dict_type(
210
+ self.actor_module_fsdp,
211
+ state_dict_type=StateDictType.SHARDED_STATE_DICT,
212
+ state_dict_config=ShardedStateDictConfig(),
213
+ )
214
+ params = self._get_actor_params()
215
+ ret = []
216
+ for key, tensor in params.items():
217
+ ret.append((key, tensor.size(), tensor.dtype))
218
+ self._weights_info = ret
219
+ return ret
220
+
221
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
222
+ def save_model_to_cpu(self, n):
223
+ if not hasattr(self, "cpu_saved_models"):
224
+ self.cpu_saved_models = {}
225
+ self.cpu_saved_models[n] = fsdp2_sharded_save_to_cpu(self.actor_module_fsdp)
226
+
227
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
228
+ def restore_model_from_cpu(self, n):
229
+ if n in self.cpu_saved_models:
230
+ cpu_sharded_state, global_spec = self.cpu_saved_models[n]
231
+ fsdp2_sharded_load_from_cpu(self.actor_module_fsdp, cpu_sharded_state, global_spec)
232
+
233
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
234
+ def clear_cpu_model(self, n):
235
+ if n in self.cpu_saved_models:
236
+ del self.cpu_saved_models[n]
237
+
238
+
239
+ class DetachAsyncRolloutWorker(DetachNcclSync):
240
+ def __init__(self, config: DictConfig, role: str):
241
+ print(f"[DetachAsyncRolloutWorker] {DetachAsyncRolloutWorker.__mro__}")
242
+ DetachNcclSync.__init__(self, config, role)
243
+
244
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
245
+ def set_actor_weights_info(self, weights_info):
246
+ assert self._is_rollout
247
+ self._weights_info = weights_info
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_main.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import os
17
+ import socket
18
+ import threading
19
+ from pprint import pprint
20
+
21
+ import hydra
22
+ import ray
23
+ from omegaconf import OmegaConf
24
+
25
+ from verl.experimental.fully_async_policy.fully_async_rollouter import FullyAsyncRollouter
26
+ from verl.experimental.fully_async_policy.fully_async_trainer import FullyAsyncTrainer
27
+ from verl.experimental.fully_async_policy.message_queue import MessageQueue, MessageQueueClient
28
+ from verl.trainer.ppo.ray_trainer import ResourcePoolManager
29
+ from verl.trainer.ppo.utils import Role, need_reference_policy
30
+ from verl.utils.fs import copy_to_local
31
+
32
+
33
+ def create_resource_pool_manager(config, roles: list) -> ResourcePoolManager:
34
+ """
35
+ Create resource pool manager
36
+
37
+ Args:
38
+ config: Configuration object
39
+ roles: List of roles that need to create resource pools
40
+
41
+ Returns:
42
+ ResourcePoolManager: Resource pool manager
43
+ """
44
+ resource_pool_spec = {}
45
+ mapping = {}
46
+
47
+ # Actor/Critic resource pool
48
+ if any(role in roles for role in [Role.Actor, Role.ActorRollout, Role.Critic, Role.RefPolicy, Role.RewardModel]):
49
+ assert config.trainer.n_gpus_per_node > 0, "config.trainer.n_gpus_per_node must be greater than 0"
50
+ assert config.trainer.nnodes > 0, "config.trainer.nnodes must be greater than 0"
51
+
52
+ trainer_pool = [config.trainer.n_gpus_per_node] * config.trainer.nnodes
53
+ resource_pool_spec["trainer_pool"] = trainer_pool
54
+
55
+ # Map training-related roles to the same resource pool
56
+ for role in [Role.Actor, Role.ActorRollout, Role.Critic, Role.RefPolicy, Role.RewardModel]:
57
+ if role in roles:
58
+ mapping[role] = "trainer_pool"
59
+
60
+ # Rollout resource pool
61
+ if Role.Rollout in roles:
62
+ assert config.rollout.n_gpus_per_node > 0, "config.rollout.n_gpus_per_node must be greater than 0"
63
+ assert config.rollout.nnodes > 0, "config.rollout.nnodes must be greater than 0"
64
+
65
+ rollout_pool = [config.rollout.n_gpus_per_node] * config.rollout.nnodes
66
+ resource_pool_spec["rollout_pool"] = rollout_pool
67
+ mapping[Role.Rollout] = "rollout_pool"
68
+
69
+ return ResourcePoolManager(resource_pool_spec=resource_pool_spec, mapping=mapping)
70
+
71
+
72
+ def create_role_worker_mapping(config):
73
+ """
74
+ Create mapping from roles to worker classes
75
+
76
+ Args:
77
+ config: Configuration object
78
+
79
+ Returns:
80
+ dict: Mapping from roles to worker classes
81
+ """
82
+ # Select worker class based on strategy
83
+ if config.actor_rollout_ref.actor.strategy in ["fsdp", "fsdp2"]:
84
+ assert config.actor_rollout_ref.actor.strategy == config.critic.strategy
85
+ from verl.experimental.fully_async_policy.fsdp_workers import (
86
+ CriticWorker,
87
+ DetachActorWorker,
88
+ DetachAsyncRolloutWorker,
89
+ )
90
+ from verl.single_controller.ray import RayWorkerGroup
91
+
92
+ ray_worker_group_cls = RayWorkerGroup
93
+
94
+ elif config.actor_rollout_ref.actor.strategy == "megatron":
95
+ assert config.critic.strategy == "megatron"
96
+ from verl.experimental.fully_async_policy.megatron_worker import (
97
+ CriticWorker,
98
+ DetachActorWorker,
99
+ DetachAsyncRolloutWorker,
100
+ )
101
+ from verl.single_controller.ray import RayWorkerGroup
102
+
103
+ ray_worker_group_cls = RayWorkerGroup
104
+ else:
105
+ raise NotImplementedError(f"Unsupported strategy: {config.actor_rollout_ref.actor.strategy}")
106
+
107
+ train_role = Role.ActorRollout if config.async_training.use_trainer_do_validate else Role.Actor
108
+ role_worker_mapping = {
109
+ train_role: ray.remote(DetachActorWorker),
110
+ Role.Rollout: ray.remote(DetachAsyncRolloutWorker),
111
+ Role.Critic: ray.remote(CriticWorker),
112
+ }
113
+
114
+ if config.reward_model.enable:
115
+ if config.reward_model.strategy in ["fsdp", "fsdp2"]:
116
+ from verl.workers.fsdp_workers import RewardModelWorker
117
+ elif config.reward_model.strategy == "megatron":
118
+ from verl.workers.megatron_workers import RewardModelWorker
119
+ else:
120
+ raise NotImplementedError
121
+
122
+ role_worker_mapping[Role.RewardModel] = ray.remote(RewardModelWorker)
123
+
124
+ # Add reference policy (if KL loss or reward is required)
125
+ if need_reference_policy(config):
126
+ role_worker_mapping[Role.RefPolicy] = ray.remote(DetachActorWorker)
127
+
128
+ return role_worker_mapping, ray_worker_group_cls
129
+
130
+
131
+ @ray.remote(num_cpus=1)
132
+ class FullyAsyncTaskRunner:
133
+ """
134
+ Ray remote class for executing distributed PPO training tasks.
135
+ """
136
+
137
+ def __init__(self):
138
+ self.running = False
139
+ self.components = {}
140
+ self.shutdown_event = threading.Event()
141
+
142
+ def run(self, config):
143
+ print("[ASYNC MAIN] Starting fully async PPO training...")
144
+ self._initialize_components(config)
145
+ self._run_training_loop()
146
+
147
+ def _initialize_components(self, config) -> None:
148
+ print(f"[ASYNC MAIN] TaskRunner hostname: {socket.gethostname()}, PID: {os.getpid()}")
149
+ pprint(OmegaConf.to_container(config, resolve=True))
150
+ OmegaConf.resolve(config)
151
+
152
+ print("[ASYNC MAIN] Initializing model and tokenizer...")
153
+ local_path = copy_to_local(
154
+ config.actor_rollout_ref.model.path, use_shm=config.actor_rollout_ref.model.get("use_shm", False)
155
+ )
156
+ from verl.utils import hf_processor, hf_tokenizer
157
+
158
+ trust_remote_code = config.data.get("trust_remote_code", False)
159
+ tokenizer = hf_tokenizer(local_path, trust_remote_code=trust_remote_code)
160
+
161
+ # Used for multimodal LLM, could be None
162
+ processor = hf_processor(local_path, trust_remote_code=trust_remote_code, use_fast=True)
163
+
164
+ self.components["tokenizer"] = tokenizer
165
+ self.components["processor"] = processor
166
+ self.components["config"] = config
167
+
168
+ print("[ASYNC MAIN] Creating worker mapping and resource pools...")
169
+ role_worker_mapping, ray_worker_group_cls = create_role_worker_mapping(config)
170
+ self.components["role_worker_mapping"] = role_worker_mapping
171
+ self.components["ray_worker_group_cls"] = ray_worker_group_cls
172
+
173
+ print("[ASYNC MAIN] Creating FullyAsyncRollouter...")
174
+ self._create_rollouter(config)
175
+
176
+ print("[ASYNC MAIN] Creating FullyAsyncTrainer...")
177
+ self._create_trainer(config)
178
+
179
+ # sync total_train_steps between rollouter and trainer
180
+ total_train_steps = ray.get(self.components["rollouter"].get_total_train_steps.remote())
181
+ print(f"total_train_steps {total_train_steps}")
182
+ ray.get(self.components["trainer"].set_total_train_steps.remote(total_train_steps))
183
+
184
+ # max_queue_size
185
+ max_queue_size = ray.get(self.components["rollouter"].get_max_queue_size.remote())
186
+ print(f"[ASYNC MAIN] Creating MessageQueue... max_queue_size {max_queue_size}")
187
+ message_queue = MessageQueue.remote(config, max_queue_size)
188
+ message_queue_client = MessageQueueClient(message_queue)
189
+ self.components["message_queue"] = message_queue
190
+ self.components["message_queue_client"] = message_queue_client
191
+
192
+ ray.get(self.components["rollouter"].set_message_queue_client.remote(self.components["message_queue_client"]))
193
+ ray.get(self.components["trainer"].set_message_queue_client.remote(self.components["message_queue_client"]))
194
+
195
+ print("[ASYNC MAIN] Setting up parameter synchronization...")
196
+ from verl.experimental.fully_async_policy.param_sync import ParameterSynchronizer
197
+
198
+ param_synchronizer = ParameterSynchronizer.remote(
199
+ config=config,
200
+ trainer=self.components["trainer"],
201
+ rollouter=self.components["rollouter"],
202
+ mq=self.components["message_queue_client"],
203
+ )
204
+ ray.get(self.components["trainer"].set_parameter_synchronizer.remote(param_synchronizer))
205
+
206
+ # load checkpoint and sync parameter before doing anything
207
+ val_before_train = config.trainer.get("val_before_train", True)
208
+ # param_version resume from ckpt or default 0
209
+ param_version = ray.get(self.components["trainer"].load_checkpoint.remote())
210
+ ray.get(self.components["rollouter"].load_checkpoint.remote())
211
+ ray.get(
212
+ param_synchronizer.sync_weights.remote(
213
+ version=param_version,
214
+ validate=val_before_train,
215
+ use_trainer_do_validate=config.async_training.use_trainer_do_validate,
216
+ )
217
+ )
218
+ ray.get(param_synchronizer.wait_last_valid.remote())
219
+
220
+ self.components["param_synchronizer"] = param_synchronizer
221
+ print("[ASYNC MAIN] All components initialized successfully")
222
+
223
+ def _create_rollouter(self, config) -> None:
224
+ rollouter = FullyAsyncRollouter.remote(
225
+ config=config,
226
+ tokenizer=self.components["tokenizer"],
227
+ role_worker_mapping={Role.Rollout: self.components["role_worker_mapping"][Role.Rollout]},
228
+ resource_pool_manager=create_resource_pool_manager(config, roles=[Role.Rollout]),
229
+ ray_worker_group_cls=self.components["ray_worker_group_cls"],
230
+ processor=self.components["processor"],
231
+ device_name=config.trainer.device,
232
+ )
233
+
234
+ ray.get(rollouter.init_workers.remote())
235
+ ray.get(rollouter.set_max_required_samples.remote())
236
+
237
+ self.components["rollouter"] = rollouter
238
+ print("[ASYNC MAIN] Rollouter created and initialized successfully")
239
+
240
+ def _create_trainer(self, config) -> None:
241
+ trainer_role_mapping = {
242
+ role: worker_cls
243
+ for role, worker_cls in self.components["role_worker_mapping"].items()
244
+ if role != Role.Rollout
245
+ }
246
+
247
+ trainer = FullyAsyncTrainer.remote(
248
+ config=config,
249
+ tokenizer=self.components["tokenizer"],
250
+ role_worker_mapping=trainer_role_mapping,
251
+ resource_pool_manager=create_resource_pool_manager(config, roles=list(trainer_role_mapping.keys())),
252
+ ray_worker_group_cls=self.components["ray_worker_group_cls"],
253
+ processor=self.components["processor"],
254
+ device_name=config.trainer.device,
255
+ )
256
+
257
+ ray.get(trainer.init_workers.remote())
258
+ self.components["trainer"] = trainer
259
+ print("[ASYNC MAIN] FullyAsyncTrainer created and initialized successfully")
260
+
261
+ def _run_training_loop(self):
262
+ self.running = True
263
+
264
+ print("[ASYNC MAIN] Starting Rollouter and Trainer...")
265
+ rollouter_future = self.components["rollouter"].fit.remote()
266
+ trainer_future = self.components["trainer"].fit.remote()
267
+
268
+ futures = [rollouter_future, trainer_future]
269
+
270
+ try:
271
+ while futures:
272
+ # Use ray.wait to monitor all futures and return when any one is completed.
273
+ done_futures, remaining_futures = ray.wait(futures, num_returns=1, timeout=None)
274
+
275
+ for future in done_futures:
276
+ try:
277
+ ray.get(future)
278
+ print("[ASYNC MAIN] One component completed successfully")
279
+ except Exception as e:
280
+ print(f"[ASYNC MAIN] Component failed with error: {e}")
281
+ for remaining_future in remaining_futures:
282
+ ray.cancel(remaining_future)
283
+ raise e
284
+
285
+ futures = remaining_futures
286
+
287
+ except Exception as e:
288
+ print(f"[ASYNC MAIN] Training failed: {e}")
289
+ for future in futures:
290
+ ray.cancel(future)
291
+ raise
292
+ finally:
293
+ asyncio.run(self.components["message_queue_client"].clear_queue())
294
+ print("[ASYNC MAIN] Training completed or interrupted")
295
+
296
+
297
+ @hydra.main(config_path="config", config_name="fully_async_ppo_trainer", version_base=None)
298
+ def main(config):
299
+ from verl.trainer.main_ppo import run_ppo
300
+
301
+ # Ensure async training config exists
302
+ if not hasattr(config, "async_training"):
303
+ raise RuntimeError("must set async_training config")
304
+ from time import time
305
+
306
+ start_time = time()
307
+ run_ppo(config, task_runner_class=FullyAsyncTaskRunner)
308
+ print(f"total time: {time() - start_time:.2f} seconds")
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main()
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_rollouter.py ADDED
@@ -0,0 +1,793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import functools
17
+ import multiprocessing
18
+ import os
19
+ import time
20
+ from concurrent.futures import ThreadPoolExecutor
21
+ from pprint import pformat
22
+
23
+ import numpy as np
24
+ import ray
25
+ import torch
26
+ from ray import ObjectRef
27
+
28
+ from verl.experimental.fully_async_policy.detach_utils import (
29
+ RolloutSample,
30
+ ValidateMetrics,
31
+ prepare_single_generation_data,
32
+ )
33
+ from verl.experimental.fully_async_policy.message_queue import MessageQueueClient
34
+ from verl.experimental.fully_async_policy.ray_trainer import FullyAsyncRayPPOTrainer
35
+ from verl.single_controller.ray import RayClassWithInitArgs, RayWorkerGroup
36
+ from verl.trainer.ppo.ray_trainer import ResourcePoolManager
37
+ from verl.trainer.ppo.reward import load_reward_manager
38
+ from verl.trainer.ppo.utils import Role, WorkerType
39
+ from verl.utils.checkpoint.checkpoint_manager import find_latest_ckpt_path
40
+ from verl.utils.profiler import marked_timer
41
+ from verl.utils.tracking import ValidationGenerationsLogger
42
+
43
+
44
+ @ray.remote(num_cpus=10, max_concurrency=100)
45
+ class FullyAsyncRollouter(FullyAsyncRayPPOTrainer):
46
+ """
47
+ Asynchronous sample generator, responsible for continuously generating training samples
48
+ and putting them into MessageQueue
49
+ Based on the mature implementation improvements of OneStepOffRayTrainer
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ config,
55
+ tokenizer,
56
+ role_worker_mapping: dict[Role, WorkerType],
57
+ resource_pool_manager: ResourcePoolManager,
58
+ ray_worker_group_cls: RayWorkerGroup = RayWorkerGroup,
59
+ processor=None,
60
+ reward_fn=None,
61
+ val_reward_fn=None,
62
+ device_name=None,
63
+ ):
64
+ # Store the tokenizer for text processing
65
+ self.tokenizer = tokenizer
66
+ self.processor = processor
67
+ self.config = config
68
+ self.reward_fn = load_reward_manager(
69
+ config, tokenizer, num_examine=0, **config.reward_model.get("reward_kwargs", {})
70
+ )
71
+ self.val_reward_fn = load_reward_manager(
72
+ config, tokenizer, num_examine=1, **config.reward_model.get("reward_kwargs", {})
73
+ )
74
+ self.hybrid_engine = config.actor_rollout_ref.hybrid_engine
75
+
76
+ assert not self.hybrid_engine
77
+ assert self.config.data.train_batch_size == 0, "train_batch_size must be zero"
78
+ assert self.config.data.gen_batch_size == 1, "gen_batch_size must be one"
79
+ assert self.config.async_training.staleness_threshold >= 0, "staleness_threshold must larger than 0"
80
+ assert self.config.async_training.trigger_parameter_sync_step >= 1, (
81
+ "trigger_parameter_sync_step must larger than 1"
82
+ )
83
+
84
+ self.role_worker_mapping = role_worker_mapping
85
+ self.resource_pool_manager = resource_pool_manager
86
+ self.ray_worker_group_cls = ray_worker_group_cls
87
+ self.device_name = device_name if device_name else self.config.trainer.device
88
+ self.validation_generations_logger = ValidationGenerationsLogger(
89
+ project_name=self.config.trainer.project_name,
90
+ experiment_name=self.config.trainer.experiment_name,
91
+ )
92
+
93
+ self.ref_in_actor = False
94
+ self.kl_ctrl_in_reward = False
95
+ self.use_critic = False
96
+ self.use_reference_policy = False
97
+ self.use_rm = False
98
+
99
+ print("[FullyAsyncRollouter] Creating datasets...")
100
+ from verl.trainer.main_ppo import create_rl_dataset, create_rl_sampler
101
+ from verl.utils.dataset.rl_dataset import collate_fn
102
+
103
+ train_dataset = create_rl_dataset(config.data.train_files, config.data, tokenizer, processor)
104
+ val_dataset = create_rl_dataset(config.data.val_files, config.data, tokenizer, processor)
105
+ train_sampler = create_rl_sampler(config.data, train_dataset)
106
+
107
+ self._validate_config()
108
+ if self.config.async_training.use_trainer_do_validate:
109
+ rollout_gpus = config.rollout.nnodes * config.rollout.n_gpus_per_node
110
+ train_gpus = config.trainer.nnodes * config.trainer.n_gpus_per_node
111
+ total_gpus = rollout_gpus + train_gpus
112
+ print(f"[FullyAsyncRollouter] split before val_dataset total len: {len(val_dataset)}")
113
+ split_dataset = val_dataset.split(total_gpus)
114
+ rollout_val_dataset0 = split_dataset[:rollout_gpus]
115
+ from torch.utils.data import ConcatDataset
116
+
117
+ val_dataset = ConcatDataset(rollout_val_dataset0)
118
+ print(f"[FullyAsyncRollouter] split after val_dataset total len: {len(val_dataset)}")
119
+ print(f"[FullyAsyncRollouter] Rollouter _create_dataloader...\n{train_dataset}\n{val_dataset}")
120
+
121
+ self._create_dataloader(train_dataset, val_dataset, collate_fn, train_sampler)
122
+
123
+ # ==================== fully async config ====================
124
+
125
+ self.total_rollout_steps = len(self.train_dataloader) * self.config.trainer.total_epochs
126
+ if self.config.rollout.total_rollout_steps is not None:
127
+ self.total_rollout_steps = min(self.config.rollout.total_rollout_steps, self.total_rollout_steps)
128
+ print(f"[FullyAsyncRollouter] Total rollout steps: {self.total_rollout_steps}")
129
+ self.total_train_steps = None
130
+
131
+ # Rollouter parameter configuration
132
+ self.message_queue_client = None
133
+
134
+ # Worker groups: rollout_wg is same to actor_rollout_wg
135
+ self.rollout_wg = None
136
+ self.actor_rollout_wg = None
137
+ self.async_rollout_manager = None
138
+
139
+ # Config
140
+ self.staleness_threshold: float = config.async_training.get("staleness_threshold", 1)
141
+ # required_samples use ppo_mini_batch_size*require_batches as the minimum number of samples.
142
+ self.require_batches = config.async_training.require_batches
143
+ self.required_samples = config.actor_rollout_ref.actor.ppo_mini_batch_size * self.require_batches
144
+ self.max_required_samples = None
145
+ self.max_concurrent_samples = None
146
+ # queue size
147
+ self.max_queue_size = None
148
+
149
+ # Statistics
150
+ self.current_param_version = 0
151
+ self.total_generated_samples = 0
152
+ self.staleness_samples = 0
153
+ self.dropped_stale_samples = 0
154
+ self.processed_sample_count = 0
155
+ # we start from step 1
156
+ self.global_steps = 1
157
+ self.idle_start_time = None
158
+ self.version_start_time = None
159
+
160
+ # Concurrency control
161
+ # Modified by self.pause() or self._should_pause_generation()
162
+ self.paused = False
163
+ self.running = True
164
+ self.monitor_loop_trigger = True
165
+
166
+ # Add dataloader lock
167
+ self.dataloader_lock = asyncio.Lock()
168
+
169
+ # Initialize async queues
170
+ self.pending_queue = asyncio.Queue(maxsize=128)
171
+ self.active_tasks = set()
172
+ self.cancel_queue = asyncio.Queue()
173
+
174
+ cpu_cores = multiprocessing.cpu_count()
175
+ # cpu case use cpu_cores; io case use cpu_cores*2
176
+ self.validate_executor = ThreadPoolExecutor(max_workers=cpu_cores)
177
+ self.parallel_validate_and_rollout = config.async_training.get("parallel_validate_and_rollout", False)
178
+ self.validate_task = None
179
+
180
+ def _init_async_objects(self):
181
+ # Initialize asyncio synchronization primitives.
182
+ # We let asyncio.Condition create the Lock internally to ensure they share the same Event Loop.
183
+ # This avoids 'ValueError: loop argument must agree with lock' which can occur in Ray environments
184
+ # where the lock's captured loop (get_running_loop) differs from Condition's default loop check.
185
+ # Explicitly passing the loop is deprecated/removed in Python 3.10+, so this reverse-initialization
186
+ # is the most robust workaround.
187
+ self.condition = asyncio.Condition()
188
+ self.lock = self.condition._lock
189
+
190
+ async def set_message_queue_client(self, message_queue_client: MessageQueueClient):
191
+ """Set message queue client"""
192
+ async with self.lock:
193
+ self.message_queue_client = message_queue_client
194
+
195
+ async def set_max_required_samples(self):
196
+ async with self.lock:
197
+ self.max_required_samples = int(
198
+ self.required_samples
199
+ * (self.staleness_threshold + 1)
200
+ * self.config.async_training.trigger_parameter_sync_step
201
+ )
202
+ self.total_train_steps = int(
203
+ self.total_rollout_steps
204
+ / (self.required_samples * self.config.async_training.trigger_parameter_sync_step)
205
+ )
206
+
207
+ self.max_concurrent_samples = len(self.async_rollout_manager.server_handles) * 16
208
+ self.max_concurrent_samples = min(self.max_concurrent_samples, self.max_required_samples)
209
+ self.max_queue_size = self.max_required_samples
210
+
211
+ print(
212
+ f"[FullyAsyncRollouter] required_samples : {self.required_samples} "
213
+ f"max_required_samples: {self.max_required_samples} "
214
+ f"max_queue_size: {self.max_queue_size} "
215
+ f"total_train_steps: {self.total_train_steps} "
216
+ f"total_rollout_steps: {self.total_rollout_steps} "
217
+ f"max_concurrent_samples: {self.max_concurrent_samples} "
218
+ )
219
+
220
+ def get_rollout_wg(self):
221
+ """Get rollout worker group"""
222
+ return self.rollout_wg
223
+
224
+ def get_max_queue_size(self):
225
+ return self.max_queue_size
226
+
227
+ def get_total_train_steps(self):
228
+ return self.total_train_steps
229
+
230
+ async def update_param_version(
231
+ self, version: int, validate: bool = False, global_steps: int = 0, use_trainer_do_validate: bool = False
232
+ ):
233
+ """Update current parameter version"""
234
+ async with self.lock:
235
+ old_version = self.current_param_version
236
+ self.current_param_version = version
237
+ # every time param change, reset staleness_samples
238
+ self.staleness_samples = (
239
+ len(self.active_tasks) + self.cancel_queue.qsize() + await self.message_queue_client.get_queue_size()
240
+ )
241
+ timing_raw = {}
242
+ idle_ratio = None
243
+ if self.idle_start_time is not None and self.version_start_time is not None:
244
+ rollout_active_time = self.idle_start_time - self.version_start_time
245
+ rollout_version_time = time.time() - self.version_start_time
246
+ idle_ratio = 1 - rollout_active_time / rollout_version_time
247
+ timing_raw["rollouter/active_time"] = rollout_active_time
248
+ timing_raw["rollouter/version_time"] = rollout_version_time
249
+ timing_raw["rollouter/idle_ratio"] = idle_ratio
250
+ self.idle_start_time = None
251
+ print(
252
+ f"[FullyAsyncRollouter][Public][update_param_version] "
253
+ f"Parameter version updated from {old_version} to {version} "
254
+ f",reset staleness_samples to: {self.staleness_samples}"
255
+ f",idle_ratio: {idle_ratio}"
256
+ )
257
+ need_validate = (
258
+ (
259
+ self.val_reward_fn is not None
260
+ and self.config.rollout.test_freq > 0
261
+ and self.current_param_version % self.config.rollout.test_freq == 0
262
+ and self.current_param_version > 0
263
+ ) # don't test here in the initial parameter sync
264
+ or (validate and self.val_reward_fn is not None)
265
+ )
266
+ print(
267
+ f"[FullyAsyncRollouter] need_validate: {need_validate},"
268
+ f"parallel_validate_and_rollout: {self.parallel_validate_and_rollout}"
269
+ )
270
+ if not need_validate:
271
+ data = ValidateMetrics(
272
+ timing_raw=timing_raw, metrics=None, global_steps=global_steps, param_version=version
273
+ )
274
+ elif need_validate and not self.parallel_validate_and_rollout:
275
+ data = self._validate_wrapper(timing_raw, version, global_steps, use_trainer_do_validate)
276
+
277
+ if not need_validate or not self.parallel_validate_and_rollout:
278
+ await self.message_queue_client.put_validate(ray.cloudpickle.dumps(data))
279
+
280
+ self.version_start_time = time.time()
281
+
282
+ if need_validate and self.parallel_validate_and_rollout:
283
+ if self.validate_task and not self.validate_task.done():
284
+ print("[FullyAsyncRollouter] validate_task is running, wait last validate_task to finish")
285
+ self.validate_task.get()
286
+ self.validate_task = asyncio.create_task(
287
+ self.do_validate_async(timing_raw, version, global_steps, use_trainer_do_validate)
288
+ )
289
+
290
+ def _validate_wrapper(
291
+ self, timing_raw: dict, version: int, global_steps: int = 0, use_trainer_do_validate: bool = False
292
+ ):
293
+ val_metrics = None
294
+ with marked_timer("rollouter/validate_time", timing_raw, color="green"):
295
+ val_metrics: dict = self._validate(use_trainer_do_validate)
296
+ data = ValidateMetrics(
297
+ timing_raw=timing_raw, metrics=val_metrics, global_steps=global_steps, param_version=version
298
+ )
299
+ return data
300
+
301
+ async def do_validate_async(
302
+ self,
303
+ timing_raw: dict,
304
+ version: int,
305
+ global_steps: int = 0,
306
+ use_trainer_do_validate: bool = False,
307
+ ):
308
+ loop = asyncio.get_running_loop()
309
+
310
+ data = await loop.run_in_executor(
311
+ self.validate_executor,
312
+ functools.partial(
313
+ self._validate_wrapper,
314
+ timing_raw=timing_raw,
315
+ version=version,
316
+ global_steps=global_steps,
317
+ use_trainer_do_validate=use_trainer_do_validate,
318
+ ),
319
+ )
320
+ await self.message_queue_client.put_validate(ray.cloudpickle.dumps(data))
321
+
322
+ async def save_checkpoint(self, local_global_step_folder: str):
323
+ # WARNING!: Due to the asynchronous nature, there are some in-flight samples
324
+ # (pending/cancel/result queue and message queue).
325
+ # Therefore, directly saving the state of the dataloader will result in losing these
326
+ # samples when resuming training.
327
+ # TODO: Implement dataloader recovery without losing in-flight samples.
328
+ from verl.utils.fs import local_mkdir_safe
329
+
330
+ # save dataloader
331
+ local_mkdir_safe(local_global_step_folder)
332
+ dataloader_local_path = os.path.join(local_global_step_folder, "data.pt")
333
+ async with self.dataloader_lock:
334
+ dataloader_state_dict = self.train_dataloader.state_dict()
335
+ torch.save(dataloader_state_dict, dataloader_local_path)
336
+ print(f"[FullyAsyncRollouter] Saved dataloader checkpoint to {dataloader_local_path}")
337
+
338
+ def load_checkpoint(self):
339
+ """Load checkpoint including dataloader state based on resume mode"""
340
+
341
+ if self.config.trainer.resume_mode == "disable":
342
+ print("[FullyAsyncRollouter] Resume mode is disabled, starting from scratch")
343
+ return 0
344
+
345
+ # Determine checkpoint folder path
346
+ if self.config.trainer.default_hdfs_dir is not None:
347
+ raise NotImplementedError("[FullyAsyncRollouter] Load from hdfs is not implemented yet")
348
+ else:
349
+ checkpoint_folder = self.config.trainer.default_local_dir
350
+ if not os.path.isabs(checkpoint_folder):
351
+ working_dir = os.getcwd()
352
+ checkpoint_folder = os.path.join(working_dir, checkpoint_folder)
353
+
354
+ global_step_folder = find_latest_ckpt_path(checkpoint_folder)
355
+
356
+ # Find and validate global_step_folder based on resume mode
357
+ if self.config.trainer.resume_mode == "auto":
358
+ if global_step_folder is None:
359
+ print("[FullyAsyncRollouter] Training from scratch (no checkpoint found)")
360
+ return 0
361
+ elif self.config.trainer.resume_mode == "resume_path":
362
+ assert isinstance(self.config.trainer.resume_from_path, str), (
363
+ "[FullyAsyncRollouter] resume_from_path must be str type"
364
+ )
365
+ assert "global_step_" in self.config.trainer.resume_from_path, (
366
+ "[FullyAsyncRollouter] resume_from_path must specify the global_steps"
367
+ )
368
+ global_step_folder = self.config.trainer.resume_from_path
369
+ if not os.path.isabs(global_step_folder):
370
+ working_dir = os.getcwd()
371
+ global_step_folder = os.path.join(working_dir, global_step_folder)
372
+ else:
373
+ raise ValueError(f"[FullyAsyncRollouter] Unknown resume_mode: {self.config.trainer.resume_mode}")
374
+
375
+ print(f"[FullyAsyncRollouter] Loading checkpoint from: {global_step_folder}")
376
+
377
+ # Extract and set global step
378
+ trainer_global_steps = int(global_step_folder.split("global_step_")[-1])
379
+ self.global_steps = (
380
+ trainer_global_steps * self.required_samples * self.config.async_training.trigger_parameter_sync_step + 1
381
+ )
382
+ print(f"[FullyAsyncRollouter] Setting global_steps to {self.global_steps}")
383
+
384
+ # Load dataloader state
385
+ dataloader_local_path = os.path.join(global_step_folder, "data.pt")
386
+ if os.path.exists(dataloader_local_path):
387
+ dataloader_state_dict = torch.load(dataloader_local_path, weights_only=False)
388
+ self.train_dataloader.load_state_dict(dataloader_state_dict)
389
+ print(f"[FullyAsyncRollouter] Loaded dataloader state from {dataloader_local_path}")
390
+ else:
391
+ print(
392
+ f"[FullyAsyncRollouter] Warning: No dataloader state found at {dataloader_local_path}, "
393
+ f"will start from scratch"
394
+ )
395
+
396
+ def _validate_config(self):
397
+ # Validate asynchronous training configuration
398
+ if not hasattr(self.config, "async_training"):
399
+ raise ValueError("[FullyAsyncRollouter] Missing async_training configuration")
400
+ assert self.config.actor_rollout_ref.rollout.calculate_log_probs, "must rollout calculate log_probs"
401
+
402
+ async def init_workers(self):
403
+ """Initialize distributed training workers using Ray backend.
404
+
405
+ Creates:
406
+ 1. Ray resource pools from configuration
407
+ 2. Worker groups for each role (actor, critic, etc.)
408
+ """
409
+ self._init_async_objects()
410
+ self._init_resource_pools()
411
+ self._create_worker_classes()
412
+ self._init_worker_groups()
413
+ self._init_models()
414
+ await self._init_async_rollout_manager()
415
+
416
+ def _create_actor_rollout_classes(self):
417
+ # only create rollout
418
+ for role in [Role.Rollout]:
419
+ resource_pool = self.resource_pool_manager.get_resource_pool(role)
420
+ role_cls = RayClassWithInitArgs(
421
+ cls=self.role_worker_mapping[role],
422
+ config=self.config.actor_rollout_ref,
423
+ role=str(role),
424
+ )
425
+ self.resource_pool_to_cls[resource_pool][str(role)] = role_cls
426
+
427
+ def _init_models(self):
428
+ self.rollout_wg = self.all_wg[str(Role.Rollout)]
429
+ self.rollout_wg.init_model()
430
+ self.actor_rollout_wg = self.rollout_wg
431
+
432
+ def _create_continuous_iterator(self):
433
+ """
434
+ Create a continuous data iterator across epoch
435
+ """
436
+ for epoch in range(self.config.rollout.total_epochs):
437
+ iterator = iter(self.train_dataloader)
438
+ for batch_dict in iterator:
439
+ yield epoch, batch_dict
440
+
441
+ async def _init_async_rollout_manager(self):
442
+ # create async rollout manager and request scheduler
443
+ assert self.config.actor_rollout_ref.rollout.mode == "async"
444
+ from verl.experimental.fully_async_policy.agent_loop import FullyAsyncAgentLoopManager
445
+
446
+ self.async_rollout_mode = True
447
+ self.async_rollout_manager = await FullyAsyncAgentLoopManager.create(
448
+ config=self.config,
449
+ worker_group=self.rollout_wg,
450
+ )
451
+
452
+ # Add samples to the pending_queue
453
+ async def _feed_samples(self):
454
+ continuous_iterator = self._create_continuous_iterator()
455
+
456
+ for epoch, batch_dict in continuous_iterator:
457
+ # Similar to _prepare_generate_batch: Separate data
458
+ full_batch = prepare_single_generation_data(batch_dict, self.config)
459
+
460
+ sample_id = f"sample_{epoch}_{self.global_steps}"
461
+
462
+ rollout_sample = RolloutSample(
463
+ full_batch=full_batch,
464
+ agent_loop_output_list=[None] * self.config.actor_rollout_ref.rollout.n,
465
+ sample_id=sample_id,
466
+ epoch=epoch,
467
+ param_version=0,
468
+ param_version_start=[],
469
+ param_version_end=[],
470
+ processing_times=[],
471
+ tool_calls=[],
472
+ rollout_status={},
473
+ )
474
+
475
+ await self.pending_queue.put(rollout_sample)
476
+
477
+ # Check if have reached the last step
478
+ if self.global_steps >= self.total_rollout_steps:
479
+ print(
480
+ f"[FullyAsyncRollouter][Feed] "
481
+ f"Maximum count has been reached, stop adding new samples"
482
+ f"{self.global_steps} >= {self.total_rollout_steps}"
483
+ )
484
+ break
485
+
486
+ self.global_steps += 1
487
+
488
+ # End signal
489
+ await self.pending_queue.put("DONE")
490
+ print(f"[FullyAsyncRollouter][Feed] Sample addition is complete, {self.global_steps} samples have been added")
491
+
492
+ async def _processor_worker(self):
493
+ """
494
+ Streaming worker coroutines, a sample is submitted for processing without waiting for batches
495
+ """
496
+ while True:
497
+ if self.paused or await self._should_pause_generation():
498
+ print(
499
+ "[FullyAsyncRollouter][Processor] Received pause signal, waiting for remaining tasks to return..."
500
+ )
501
+ async with self.lock:
502
+ self.paused = True
503
+ while self.active_tasks:
504
+ async with self.lock:
505
+ # After acquiring the lock, the number of active_tasks may change, need to be verified again
506
+ if self.active_tasks:
507
+ done_tasks, self.active_tasks = await asyncio.wait(
508
+ self.active_tasks, return_when=asyncio.FIRST_COMPLETED
509
+ )
510
+ for task in done_tasks:
511
+ await task
512
+
513
+ async with self.lock:
514
+ while self.paused:
515
+ self.idle_start_time = time.time()
516
+ await self.condition.wait()
517
+ continue
518
+
519
+ simple_from_cancel_queue = False
520
+ if not self.cancel_queue.empty():
521
+ rollout_sample = await self.cancel_queue.get()
522
+ simple_from_cancel_queue = True
523
+ else:
524
+ rollout_sample = await self.pending_queue.get()
525
+ self.staleness_samples += 1
526
+
527
+ if rollout_sample == "DONE":
528
+ print(
529
+ "[FullyAsyncRollouter][Processor] Received end signal, waiting for remaining tasks to complete..."
530
+ )
531
+ while self.active_tasks:
532
+ async with self.lock:
533
+ if self.active_tasks:
534
+ done_tasks, self.active_tasks = await asyncio.wait(
535
+ self.active_tasks, return_when=asyncio.FIRST_COMPLETED
536
+ )
537
+ for task in done_tasks:
538
+ await task
539
+ break
540
+
541
+ # Check whether the number of concurrent tasks exceeds the limit
542
+ while len(self.active_tasks) >= self.max_concurrent_samples:
543
+ async with self.lock:
544
+ if self.active_tasks:
545
+ done_tasks, self.active_tasks = await asyncio.wait(
546
+ self.active_tasks, return_when=asyncio.FIRST_COMPLETED
547
+ )
548
+ for task in done_tasks:
549
+ await task
550
+
551
+ # Submit single sample processing
552
+ async with self.lock:
553
+ # After the pause is over, the lock is acquired and it is necessary
554
+ # to determine whether it is the pause phase, otherwise continue to wait
555
+ while self.paused:
556
+ await self.condition.wait()
557
+ task = asyncio.create_task(
558
+ self._process_single_sample_streaming(rollout_sample),
559
+ name=rollout_sample.sample_id,
560
+ )
561
+ self.active_tasks.add(task)
562
+
563
+ if simple_from_cancel_queue:
564
+ self.cancel_queue.task_done()
565
+ else:
566
+ self.pending_queue.task_done()
567
+
568
+ async def _process_single_sample_streaming(self, rollout_sample: RolloutSample):
569
+ """Process a single sample streamingly"""
570
+ # Calling asynchronous generation methods
571
+ rollout_sample.full_batch.non_tensor_batch["param_version"] = [self.current_param_version] * len(
572
+ rollout_sample.full_batch
573
+ )
574
+ ret, is_cancel = await self.async_rollout_manager.generate_single_sample_async(
575
+ rollout_sample.full_batch, rollout_sample.agent_loop_output_list
576
+ )
577
+ if not is_cancel:
578
+ rollout_sample.full_batch = ret
579
+ rollout_sample.full_batch.non_tensor_batch["uid"] = np.array(
580
+ [f"uid_{rollout_sample.sample_id}"] * len(rollout_sample.full_batch), dtype=object
581
+ )
582
+ rollout_sample.param_version = self.current_param_version
583
+ rollout_sample.rollout_status = await self.get_statistics()
584
+ rollout_sample.agent_loop_output_list = []
585
+
586
+ success = await self.message_queue_client.put_sample(
587
+ sample=ray.cloudpickle.dumps(rollout_sample),
588
+ param_version=rollout_sample.param_version,
589
+ )
590
+ if success:
591
+ self.total_generated_samples += 1
592
+ else:
593
+ self.dropped_stale_samples += 1
594
+ else:
595
+ rollout_sample.agent_loop_output_list = ret
596
+ await self.cancel_queue.put(rollout_sample)
597
+
598
+ self.processed_sample_count += 1
599
+
600
+ async def _streaming_generation_main(self):
601
+ """The main entry method for stream processing"""
602
+
603
+ if self.async_rollout_manager is None:
604
+ await self._init_async_rollout_manager()
605
+
606
+ # Start the streaming loop
607
+ print(f"[FullyAsyncRollouter] Start streaming mode, maximum concurrent samples: {self.max_concurrent_samples}")
608
+
609
+ # Start sample feed coroutine, streaming process coroutine
610
+ self.feed_task = asyncio.create_task(self._feed_samples())
611
+ self.processor_task = asyncio.create_task(self._processor_worker())
612
+
613
+ try:
614
+ # Wait for sample feed to complete
615
+ # Use asyncio.wait to monitor all tasks. If processor exits early,
616
+ # detect it instead of blocking on feed_task (it might be stuck on a full queue).
617
+ done, pending = await asyncio.wait(
618
+ [self.feed_task, self.processor_task], return_when=asyncio.FIRST_COMPLETED
619
+ )
620
+
621
+ for task in done:
622
+ if task.exception():
623
+ raise task.exception()
624
+
625
+ if self.feed_task not in done:
626
+ raise RuntimeError("Processor task exited prematurely")
627
+
628
+ print("[FullyAsyncRollouter] Sample feed completed")
629
+
630
+ # Wait for streaming to complete
631
+ await self.processor_task
632
+ print("[FullyAsyncRollouter] Streaming process completed")
633
+
634
+ except Exception as e:
635
+ print(f"[FullyAsyncRollouter] Streaming process exception:{e}")
636
+
637
+ finally:
638
+ if self.processor_task:
639
+ self.processor_task.cancel()
640
+
641
+ await asyncio.gather(self.processor_task, return_exceptions=True)
642
+
643
+ # Send a finish signal
644
+ await self.message_queue_client.put_sample(
645
+ sample=None,
646
+ param_version=self.current_param_version,
647
+ )
648
+
649
+ async with self.lock:
650
+ self.running = False
651
+
652
+ async def fit(self):
653
+ """
654
+ Start the async rollouter - entry point that sets up and runs async tasks
655
+ Main async fit method that coordinates all coroutines
656
+ """
657
+
658
+ print("[FullyAsyncRollouter] Starting FullyAsyncRollouter...")
659
+
660
+ if self.message_queue_client is None:
661
+ raise ValueError("MessageQueue client not set. Call set_message_queue_client() first.")
662
+
663
+ # Set the running status flag
664
+ async with self.lock:
665
+ self.paused = False
666
+ self.running = True
667
+
668
+ # Create the main asynchronous task
669
+ generation_task = asyncio.create_task(self._streaming_generation_main())
670
+ monitor_task = asyncio.create_task(self._async_monitor_loop())
671
+
672
+ try:
673
+ # Run build and monitoring tasks concurrently
674
+ await asyncio.gather(generation_task, monitor_task, return_exceptions=True)
675
+ except Exception as e:
676
+ print(f"[FullyAsyncRollouter] Asynchronous task execution error: {e}")
677
+ finally:
678
+ if not generation_task.done():
679
+ generation_task.cancel()
680
+ if not monitor_task.done():
681
+ monitor_task.cancel()
682
+
683
+ # Wait for the task to complete
684
+ await asyncio.gather(generation_task, monitor_task, return_exceptions=True)
685
+
686
+ print("[FullyAsyncRollouter] Rollouter fit completed")
687
+
688
+ async def _async_monitor_loop(self):
689
+ """
690
+ Async coroutine for monitoring:
691
+ Function 1: Log information output
692
+ Function 2: Trigger rollout recovery
693
+ """
694
+ last_stats_time = time.time()
695
+ stats_interval = 60.0
696
+ check_interval = 10.0
697
+
698
+ while True:
699
+ async with self.lock:
700
+ if not self.running:
701
+ break
702
+ await asyncio.sleep(check_interval)
703
+ # Print statistics periodically
704
+ current_time = time.time()
705
+ if current_time - last_stats_time >= stats_interval:
706
+ stats = await self.get_statistics()
707
+ print(f"[FullyAsyncRollouter][MonitorLoop][Statistics] {pformat(stats)}")
708
+ last_stats_time = current_time
709
+
710
+ # Trigger rollout recovery
711
+ if self.monitor_loop_trigger:
712
+ if not await self._should_pause_generation():
713
+ async with self.lock:
714
+ self.paused = False
715
+ self.condition.notify_all()
716
+
717
+ async def _should_pause_generation(self) -> bool:
718
+ """Determine whether the build should be paused"""
719
+ queue_stats = self.message_queue_client.get_statistics_sync()
720
+ queue_size = queue_stats["queue_size"]
721
+
722
+ if queue_size >= self.max_queue_size:
723
+ if not self.paused:
724
+ print(
725
+ f"[FullyAsyncRollouter][ShouldPause] "
726
+ f"due to full queue: size={queue_size}, max={self.max_queue_size}"
727
+ )
728
+ return True
729
+
730
+ if self.staleness_samples >= self.max_required_samples:
731
+ if not self.paused:
732
+ print(
733
+ "[FullyAsyncRollouter][ShouldPause] "
734
+ f"due to "
735
+ f"staleness_samples {self.staleness_samples} >= max_required_samples {self.max_required_samples} "
736
+ )
737
+ return True
738
+
739
+ return False
740
+
741
+ async def pause(self):
742
+ """pause rollout"""
743
+ print("[FullyAsyncRollouter][Public][Pause] partial rollout:", self.config.async_training.partial_rollout)
744
+ async with self.lock:
745
+ self.paused = True
746
+ # Cancel all rollout tasks
747
+ if self.config.async_training.partial_rollout:
748
+ await self.async_rollout_manager.cancel()
749
+ print("[FullyAsyncRollouter][Public][Pause] Unfinished rollout tasks canceled")
750
+ if self.active_tasks:
751
+ await asyncio.gather(*self.active_tasks, return_exceptions=True)
752
+ self.active_tasks.clear()
753
+ print("[FullyAsyncRollouter][Public][Pause] All active tasks completed")
754
+ print("[FullyAsyncRollouter][Public][Pause] Prefix cache reset")
755
+ # Always clear KV cache to release GPU memory during weight synchronization,
756
+ # regardless of partial_rollout setting.
757
+ await self.async_rollout_manager.clear_kv_cache()
758
+ self.monitor_loop_trigger = False
759
+
760
+ async def resume(self, dependency_ref: ObjectRef = None):
761
+ if dependency_ref is not None:
762
+ ray.get(dependency_ref)
763
+ print("[FullyAsyncRollouter][Public][Resume]")
764
+ async with self.lock:
765
+ if self.config.async_training.partial_rollout:
766
+ await self.async_rollout_manager.resume()
767
+ self.paused = False
768
+ self.monitor_loop_trigger = True
769
+ self.condition.notify_all()
770
+
771
+ async def get_statistics(self) -> dict:
772
+ queue_stats = self.message_queue_client.get_statistics_sync()
773
+
774
+ stats = {
775
+ # monitor stats
776
+ "monitor/active_tasks_size": len(self.active_tasks),
777
+ "monitor/queue/pending_queue_size": self.pending_queue.qsize(),
778
+ "monitor/queue/cancel_queue_size": self.cancel_queue.qsize(),
779
+ "monitor/queue/mq_queue_size": queue_stats["queue_size"],
780
+ # counting stats
781
+ "count/current_param_version": self.current_param_version,
782
+ "count/total_generated_samples": self.total_generated_samples,
783
+ "count/staleness_samples": self.staleness_samples,
784
+ "count/dropped_stale_samples": self.dropped_stale_samples,
785
+ # static stats
786
+ "static/max_required_samples": self.max_required_samples,
787
+ "static/required_samples": self.required_samples,
788
+ "static/staleness_threshold": self.staleness_threshold,
789
+ "static/max_queue_size": self.max_queue_size,
790
+ "static/max_concurrent_samples": self.max_concurrent_samples,
791
+ }
792
+
793
+ return stats
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_trainer.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import time
17
+ from datetime import datetime
18
+ from pprint import pprint
19
+ from typing import Any
20
+
21
+ import ray
22
+ from omegaconf import OmegaConf
23
+ from tqdm import tqdm
24
+
25
+ from verl.experimental.fully_async_policy.detach_utils import (
26
+ MetricsAggregator,
27
+ ValidateMetrics,
28
+ assemble_batch_from_rollout_samples,
29
+ )
30
+ from verl.experimental.fully_async_policy.message_queue import MessageQueueClient
31
+ from verl.experimental.fully_async_policy.ray_trainer import FullyAsyncRayPPOTrainer
32
+ from verl.single_controller.ray import RayClassWithInitArgs, RayWorkerGroup
33
+ from verl.trainer.ppo import core_algos
34
+ from verl.trainer.ppo.ray_trainer import ResourcePoolManager
35
+ from verl.trainer.ppo.reward import load_reward_manager
36
+ from verl.trainer.ppo.utils import Role, WorkerType, need_critic, need_reference_policy, need_reward_model
37
+ from verl.utils.checkpoint.checkpoint_manager import find_latest_ckpt_path, should_save_ckpt_esi
38
+ from verl.utils.debug import marked_timer
39
+
40
+
41
+ @ray.remote(num_cpus=10)
42
+ class FullyAsyncTrainer(FullyAsyncRayPPOTrainer):
43
+ """
44
+ A fully asynchronous PPO trainer that obtains samples from a MessageQueue for training.
45
+ Based on an improved implementation of OneStepOffRayTrainer
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ config,
51
+ tokenizer,
52
+ role_worker_mapping: dict[Role, WorkerType],
53
+ resource_pool_manager: ResourcePoolManager,
54
+ ray_worker_group_cls: RayWorkerGroup = RayWorkerGroup,
55
+ processor=None,
56
+ reward_fn=None,
57
+ val_reward_fn=None,
58
+ device_name=None,
59
+ ):
60
+ # Store the tokenizer for text processing
61
+ self.tokenizer = tokenizer
62
+ self.processor = processor
63
+ self.config = config
64
+ self.reward_fn = load_reward_manager(
65
+ config, tokenizer, num_examine=0, **config.reward_model.get("reward_kwargs", {})
66
+ )
67
+ self.val_reward_fn = load_reward_manager(
68
+ config, tokenizer, num_examine=1, **config.reward_model.get("reward_kwargs", {})
69
+ )
70
+
71
+ self.hybrid_engine = config.actor_rollout_ref.hybrid_engine
72
+ assert not self.hybrid_engine
73
+
74
+ self.role_worker_mapping = role_worker_mapping
75
+ self.resource_pool_manager = resource_pool_manager
76
+ self.use_reference_policy = need_reference_policy(self.config)
77
+ self.use_rm = need_reward_model(self.role_worker_mapping)
78
+ self.use_critic = need_critic(self.config)
79
+ self.ray_worker_group_cls = ray_worker_group_cls
80
+ self.device_name = device_name if device_name else self.config.trainer.device
81
+
82
+ lora_rank = config.actor_rollout_ref.model.get("lora", {}).get("rank", 0)
83
+ if lora_rank <= 0:
84
+ lora_rank = config.actor_rollout_ref.model.get("lora_rank", 0)
85
+ # if ref_in_actor is True, the reference policy will be actor without lora applied
86
+ self.ref_in_actor = lora_rank > 0
87
+
88
+ # define in-reward KL control
89
+ # kl loss control currently not suppoorted
90
+ if self.config.algorithm.use_kl_in_reward:
91
+ self.kl_ctrl_in_reward = core_algos.get_kl_controller(self.config.algorithm.kl_ctrl)
92
+
93
+ # ==================== fully async config ====================
94
+
95
+ self.message_queue_client = None
96
+ self.param_synchronizer = None
97
+
98
+ # Statistics
99
+ # we start from step 1
100
+ self.global_steps = 1
101
+ self.local_trigger_step = 1
102
+ self.processed_samples = 0
103
+ self.stale_samples_processed = 0
104
+ self.stale_trajectory_processed = 0
105
+ self.current_param_version = 0
106
+ self.total_train_steps = None
107
+ self.progress_bar = None
108
+ self.trigger_parameter_sync_step = config.async_training.trigger_parameter_sync_step
109
+ self.last_ckpt_version = 0
110
+ self.train_val_metrics = None
111
+ self.train_role = Role.ActorRollout if config.async_training.use_trainer_do_validate else Role.Actor
112
+
113
+ # required_samples use ppo_mini_batch_size*require_batches as the minimum number of samples.
114
+ self.require_batches = config.async_training.require_batches
115
+ self.required_samples = config.actor_rollout_ref.actor.ppo_mini_batch_size * self.require_batches
116
+ self.compute_prox_log_prob = self.config.async_training.compute_prox_log_prob
117
+ total_gpus = (
118
+ config.trainer.nnodes * config.trainer.n_gpus_per_node
119
+ + config.rollout.nnodes * config.rollout.n_gpus_per_node
120
+ )
121
+ self.metrics_aggregator = MetricsAggregator(total_gpus=total_gpus)
122
+
123
+ # use trainer to do validation
124
+ if self.config.async_training.use_trainer_do_validate:
125
+ from verl.trainer.main_ppo import create_rl_dataset
126
+ from verl.utils.dataset.rl_dataset import collate_fn
127
+
128
+ val_dataset = create_rl_dataset(config.data.val_files, config.data, tokenizer, processor)
129
+ rollout_gpus = config.rollout.nnodes * config.rollout.n_gpus_per_node
130
+ print(f"[FullyAsyncTrainer] split before val_dataset total len: {len(val_dataset)}")
131
+ split_dataset = val_dataset.split(total_gpus)
132
+ rollout_val_dataset0 = split_dataset[rollout_gpus:]
133
+ from torch.utils.data import ConcatDataset
134
+
135
+ val_dataset = ConcatDataset(rollout_val_dataset0)
136
+ print(f"[FullyAsyncTrainer] split after val_dataset total len: {len(val_dataset)}")
137
+ self.val_dataset = val_dataset
138
+ # update val_dataloader
139
+ val_batch_size = self.config.data.val_batch_size # Prefer config value if set
140
+ if val_batch_size is None:
141
+ val_batch_size = len(val_dataset)
142
+ from torchdata.stateful_dataloader import StatefulDataLoader
143
+
144
+ print(f"[FullyAsyncTrainer] create val_dataloader with batch_size: {val_batch_size}")
145
+ self.val_dataloader = StatefulDataLoader(
146
+ dataset=val_dataset,
147
+ batch_size=val_batch_size,
148
+ num_workers=self.config.data["dataloader_num_workers"],
149
+ shuffle=self.config.data.get("validation_shuffle", True),
150
+ drop_last=False,
151
+ collate_fn=collate_fn,
152
+ )
153
+
154
+ def set_message_queue_client(self, message_queue_client: MessageQueueClient):
155
+ """Set message queue client"""
156
+ self.message_queue_client = message_queue_client
157
+
158
+ def set_parameter_synchronizer(self, param_synchronizer):
159
+ """Set parameter synchronizer"""
160
+ self.param_synchronizer = param_synchronizer
161
+
162
+ def set_total_train_steps(self, total_train_steps):
163
+ self.total_train_steps = total_train_steps
164
+ self.progress_bar = tqdm(total=self.total_train_steps, initial=0, desc="Training Progress")
165
+
166
+ def get_actor_wg(self):
167
+ """Get actor worker group"""
168
+ return self.actor_wg
169
+
170
+ def _get_samples_from_queue(self) -> tuple[None, None] | tuple[int, Any]:
171
+ """
172
+ Get samples from message queue and compose gen_batch_output
173
+ Uses a loop to continuously collect samples until enough are gathered
174
+
175
+ Returns:
176
+ tuple: (epoch, batch_dict, gen_batch_output)
177
+ """
178
+ print(
179
+ f"[FullyAsyncTrainer] Requesting {self.required_samples} samples from queue",
180
+ flush=True,
181
+ )
182
+
183
+ # Collect samples using a simple loop calling get_sample
184
+ consumer_start = time.time()
185
+ queue_samples = []
186
+ queue_len = 0
187
+ while len(queue_samples) < self.required_samples:
188
+ # Get a single sample and wait until there is a sample or None is received
189
+ sample, queue_len = self.message_queue_client.get_sample_sync()
190
+
191
+ if sample is None:
192
+ print(
193
+ f"[FullyAsyncTrainer] Detected termination signal (None), stopping sample collection. "
194
+ f"Collected {len(queue_samples)}/{self.required_samples} samples"
195
+ )
196
+ break
197
+
198
+ queue_samples.append(sample)
199
+
200
+ if len(queue_samples) % 64 == 0:
201
+ print(
202
+ f"[FullyAsyncTrainer] Collected {len(queue_samples)}/{self.required_samples} samples. "
203
+ f"mq_len: {queue_len}"
204
+ )
205
+
206
+ consumer_end = time.time()
207
+
208
+ if not queue_samples or len(queue_samples) < self.required_samples:
209
+ print("[FullyAsyncTrainer] not enough samples collected after loop")
210
+ return None, None
211
+ total_wait_time = consumer_end - consumer_start
212
+
213
+ print(
214
+ f"[FullyAsyncTrainer] Loop collection completed: {len(queue_samples)}/{self.required_samples} samples, "
215
+ f"total wait time: {total_wait_time:.2f} seconds."
216
+ f"mq_len: {queue_len}"
217
+ )
218
+
219
+ queue_samples = [ray.cloudpickle.loads(x) for x in queue_samples]
220
+ # Assemble batch - now working directly with RolloutSample objects
221
+ if self.config.trainer.balance_batch:
222
+ batch = assemble_batch_from_rollout_samples(queue_samples, self.tokenizer, self.config, self._balance_batch)
223
+ else:
224
+ batch = assemble_batch_from_rollout_samples(queue_samples, self.tokenizer, self.config, None)
225
+
226
+ batch.meta_info["fully_async/total_wait_time"] = total_wait_time
227
+ return 0, batch
228
+
229
+ def _create_actor_rollout_classes(self):
230
+ # create actor
231
+ for role in [self.train_role]:
232
+ resource_pool = self.resource_pool_manager.get_resource_pool(role)
233
+ role_cls = RayClassWithInitArgs(
234
+ cls=self.role_worker_mapping[role],
235
+ config=self.config.actor_rollout_ref,
236
+ role=str(role),
237
+ )
238
+ self.resource_pool_to_cls[resource_pool][str(role)] = role_cls
239
+
240
+ def _init_models(self):
241
+ if self.use_critic:
242
+ self.critic_wg = self.all_wg[str(Role.Critic)]
243
+ self.critic_wg.init_model()
244
+
245
+ if self.use_reference_policy and not self.ref_in_actor:
246
+ self.ref_policy_wg = self.all_wg[str(Role.RefPolicy)]
247
+ self.ref_policy_wg.init_model()
248
+
249
+ if self.use_rm:
250
+ self.rm_wg = self.all_wg[str(Role.RewardModel)]
251
+ self.rm_wg.init_model()
252
+
253
+ self.actor_wg = self.all_wg[str(self.train_role)]
254
+ self.actor_wg.init_model()
255
+ self.actor_rollout_wg = self.actor_wg # to be compatible with the functions that not be modified
256
+
257
+ async def init_workers(self):
258
+ """Initialize distributed training workers using Ray backend.
259
+ Creates:
260
+ 1. Ray resource pools from configuration
261
+ 2. Worker groups for each role (actor, critic, etc.)
262
+ """
263
+ # self._init_async_objects()
264
+ self._init_resource_pools()
265
+ self._create_worker_classes()
266
+ self._init_worker_groups()
267
+ self._init_models()
268
+ await self._init_async_rollout_manager()
269
+
270
+ async def _init_async_rollout_manager(self):
271
+ # use async rollout do validate
272
+ print(f"[FullyAsyncTrainer] use_trainer_do_validate: {self.config.async_training.use_trainer_do_validate}")
273
+ if self.config.async_training.use_trainer_do_validate:
274
+ assert self.config.actor_rollout_ref.rollout.mode == "async"
275
+ self.async_rollout_mode = True
276
+ print("[FullyAsyncTrainer] Init async rollout manager")
277
+ from verl.experimental.fully_async_policy.agent_loop import FullyAsyncAgentLoopManager
278
+
279
+ self.async_rollout_manager = await FullyAsyncAgentLoopManager.create(
280
+ config=self.config, worker_group=self.actor_rollout_wg
281
+ )
282
+ print("[FullyAsyncTrainer] async_rollout_manager sleep")
283
+ await self.async_rollout_manager.sleep()
284
+ else:
285
+ print("[FullyAsyncTrainer] Skip async rollout manager (use_trainer_do_validate=False)")
286
+
287
+ async def fit(self):
288
+ """
289
+ The training loop of PPO.
290
+ The driver process only need to call the compute functions of the worker group through RPC
291
+ to construct the PPO dataflow.
292
+ The light-weight advantage computation is done on the driver process.
293
+ """
294
+ print("[FullyAsyncTrainer] Starting FullyAsyncTrainer...")
295
+ if self.message_queue_client is None:
296
+ raise ValueError("MessageQueue client not set. Call set_message_queue_client() first.")
297
+ if self.param_synchronizer is None:
298
+ raise ValueError("param_synchronizer client not set. Call set_parameter_synchronizer() first.")
299
+
300
+ from verl.utils.tracking import Tracking
301
+
302
+ self.logger = Tracking(
303
+ project_name=self.config.trainer.project_name,
304
+ experiment_name=self.config.trainer.experiment_name,
305
+ default_backend=self.config.trainer.logger,
306
+ config=OmegaConf.to_container(self.config, resolve=True),
307
+ )
308
+
309
+ self.max_steps_duration = 0
310
+
311
+ # get validate data before training
312
+ self._log_validation_data()
313
+
314
+ # Use queue mode, no need for traditional dataloader iterator
315
+ # Initialize to get the first batch of data
316
+ while True:
317
+ metrics = {}
318
+ timing_raw = {}
319
+
320
+ with marked_timer("step", timing_raw):
321
+ with marked_timer("gen", timing_raw, color="red"):
322
+ epoch, batch = self._get_samples_from_queue()
323
+ if batch is None:
324
+ break
325
+ self._collect_metrics_from_samples(batch, metrics)
326
+ batch, reward_extra_infos_dict = self._process_batch_common(
327
+ batch, metrics, timing_raw, self.local_trigger_step if self.compute_prox_log_prob else None
328
+ )
329
+ self._log_rollout(batch, reward_extra_infos_dict, timing_raw)
330
+
331
+ self._collect_metrics(batch, 0, metrics, timing_raw)
332
+ self.metrics_aggregator.add_step_metrics(
333
+ metrics=metrics, sample_count=self.required_samples, timestamp=time.time()
334
+ )
335
+ # Trigger parameter synchronization after training step
336
+ time_str = datetime.now().strftime("%H:%M:%S.%f")[:-3]
337
+ print(
338
+ f"[FullyAsyncTrainer] global_steps: {self.global_steps} "
339
+ f"local_trigger_step: {self.local_trigger_step} "
340
+ f"trigger_parameter_sync_step: {self.trigger_parameter_sync_step} "
341
+ f"{time_str}"
342
+ )
343
+ await self._trigger_parameter_sync_after_step(global_steps=self.global_steps)
344
+ self._log_validation_data()
345
+ self._check_save_checkpoint(timing_raw)
346
+ self.global_steps += 1
347
+
348
+ # final parameter sync and validate
349
+ # 1. waiting remaining validate task
350
+ ray.get(self.param_synchronizer.wait_last_valid.remote())
351
+ self._log_validation_data()
352
+ # 2. perform addtional parameter_sync and validate if trainer already updated
353
+ if self.current_param_version % self.config.rollout.test_freq != 0 or self.local_trigger_step > 1:
354
+ await self._trigger_parameter_sync_after_step(validate=True, global_steps=self.global_steps)
355
+ ray.get(self.param_synchronizer.wait_last_valid.remote())
356
+ self._log_validation_data()
357
+ self.progress_bar.close()
358
+
359
+ self._check_save_checkpoint(timing_raw)
360
+
361
+ def _check_save_checkpoint(self, timing_raw):
362
+ if self.current_param_version == self.last_ckpt_version:
363
+ return
364
+ # Check if the ESI (Elastic Server Instance)/training plan is close to expiration.
365
+ esi_close_to_expiration = should_save_ckpt_esi(
366
+ max_steps_duration=self.max_steps_duration,
367
+ redundant_time=self.config.trainer.esi_redundant_time,
368
+ )
369
+ # Check if the conditions for saving a checkpoint are met.
370
+ # The conditions include a mandatory condition (1) and
371
+ # one of the following optional conditions (2/3/4):
372
+ # 1. The save frequency is set to a positive value.
373
+ # 2. The current step number is a multiple of the save frequency.
374
+ # 3. The ESI(Elastic Server Instance)/training plan is close to expiration.
375
+ if self.config.trainer.save_freq > 0 and (
376
+ self.current_param_version % self.config.trainer.save_freq == 0 or esi_close_to_expiration
377
+ ):
378
+ if esi_close_to_expiration:
379
+ print("Force saving checkpoint: ESI instance expiration approaching.")
380
+ with marked_timer("save_checkpoint", timing_raw, color="green"):
381
+ self._save_checkpoint()
382
+ self.last_ckpt_version = self.current_param_version
383
+
384
+ def _save_checkpoint(self):
385
+ # Warning: Currently, to align the training process and metrics of colocate,
386
+ # we use current_param_version instead of global step.
387
+ # This can be logically aligned with the original self.global_steps of colocate
388
+ # and is used for metrics and ckpt. which means that the parameter synchronization
389
+ # from trainer to rollouter will increase by 1 each time.
390
+
391
+ # path: given_path + `/global_step_{global_steps}` + `/actor`
392
+ local_global_step_folder = os.path.join(
393
+ self.config.trainer.default_local_dir, f"global_step_{self.current_param_version}"
394
+ )
395
+
396
+ print(f"[FullyAsyncTrainer] local_global_step_folder: {local_global_step_folder}")
397
+ actor_local_path = os.path.join(local_global_step_folder, "actor")
398
+
399
+ actor_remote_path = (
400
+ None
401
+ if self.config.trainer.default_hdfs_dir is None
402
+ else os.path.join(
403
+ self.config.trainer.default_hdfs_dir, f"global_step_{self.current_param_version}", "actor"
404
+ )
405
+ )
406
+
407
+ remove_previous_ckpt_in_save = self.config.trainer.get("remove_previous_ckpt_in_save", False)
408
+ if remove_previous_ckpt_in_save:
409
+ print(
410
+ "[FullyAsyncTrainer] Warning: remove_previous_ckpt_in_save is deprecated,"
411
+ + " set max_actor_ckpt_to_keep=1 and max_critic_ckpt_to_keep=1 instead"
412
+ )
413
+ max_actor_ckpt_to_keep = (
414
+ self.config.trainer.get("max_actor_ckpt_to_keep", None) if not remove_previous_ckpt_in_save else 1
415
+ )
416
+ max_critic_ckpt_to_keep = (
417
+ self.config.trainer.get("max_critic_ckpt_to_keep", None) if not remove_previous_ckpt_in_save else 1
418
+ )
419
+
420
+ self.actor_rollout_wg.save_checkpoint(
421
+ actor_local_path, actor_remote_path, self.current_param_version, max_ckpt_to_keep=max_actor_ckpt_to_keep
422
+ )
423
+
424
+ if self.use_critic:
425
+ critic_local_path = os.path.join(local_global_step_folder, str(Role.Critic))
426
+ critic_remote_path = (
427
+ None
428
+ if self.config.trainer.default_hdfs_dir is None
429
+ else os.path.join(
430
+ self.config.trainer.default_hdfs_dir, f"global_step_{self.current_param_version}", str(Role.Critic)
431
+ )
432
+ )
433
+ self.critic_wg.save_checkpoint(
434
+ critic_local_path,
435
+ critic_remote_path,
436
+ self.current_param_version,
437
+ max_ckpt_to_keep=max_critic_ckpt_to_keep,
438
+ )
439
+ ray.get(self.param_synchronizer.rollouter_save_checkpoint.remote(local_global_step_folder))
440
+ # latest checkpointed iteration tracker (for atomic usage)
441
+ local_latest_checkpointed_iteration = os.path.join(
442
+ self.config.trainer.default_local_dir, "latest_checkpointed_iteration.txt"
443
+ )
444
+ with open(local_latest_checkpointed_iteration, "w") as f:
445
+ f.write(str(self.current_param_version))
446
+
447
+ def load_checkpoint(self):
448
+ if self.config.trainer.resume_mode == "disable":
449
+ # NOTE: while there is no checkpoint to load, we still need to offload the model and optimizer to CPU
450
+ self.actor_rollout_wg.load_checkpoint(None)
451
+ return 0
452
+
453
+ # load from hdfs
454
+ if self.config.trainer.default_hdfs_dir is not None:
455
+ raise NotImplementedError("load from hdfs is not implemented yet")
456
+ else:
457
+ checkpoint_folder = self.config.trainer.default_local_dir # TODO: check path
458
+ if not os.path.isabs(checkpoint_folder):
459
+ working_dir = os.getcwd()
460
+ checkpoint_folder = os.path.join(working_dir, checkpoint_folder)
461
+ global_step_folder = find_latest_ckpt_path(checkpoint_folder) # None if no latest
462
+
463
+ # find global_step_folder
464
+ if self.config.trainer.resume_mode == "auto":
465
+ if global_step_folder is None:
466
+ print("[FullyAsyncTrainer] Training from scratch")
467
+ self.actor_rollout_wg.load_checkpoint(None)
468
+ return 0
469
+ else:
470
+ if self.config.trainer.resume_mode == "resume_path":
471
+ assert isinstance(self.config.trainer.resume_from_path, str), "resume ckpt must be str type"
472
+ assert "global_step_" in self.config.trainer.resume_from_path, (
473
+ "resume ckpt must specify the global_steps"
474
+ )
475
+ global_step_folder = self.config.trainer.resume_from_path
476
+ if not os.path.isabs(global_step_folder):
477
+ working_dir = os.getcwd()
478
+ global_step_folder = os.path.join(working_dir, global_step_folder)
479
+ print(f"[FullyAsyncTrainer] Load from checkpoint folder: {global_step_folder}")
480
+ # set global step
481
+ self.current_param_version = int(global_step_folder.split("global_step_")[-1])
482
+ self.global_steps = self.current_param_version * self.trigger_parameter_sync_step + 1
483
+ self.last_ckpt_version = self.current_param_version
484
+ print(
485
+ f"[FullyAsyncTrainer] Setting global step to {self.global_steps}, "
486
+ f"current_param_version to {self.current_param_version}"
487
+ )
488
+ print(f"[FullyAsyncTrainer] Resuming from {global_step_folder}")
489
+
490
+ actor_path = os.path.join(global_step_folder, "actor")
491
+ critic_path = os.path.join(global_step_folder, str(Role.Critic))
492
+ # load actor
493
+ self.actor_rollout_wg.load_checkpoint(
494
+ actor_path, del_local_after_load=self.config.trainer.del_local_ckpt_after_load
495
+ )
496
+ # load critic
497
+ if self.use_critic:
498
+ self.critic_wg.load_checkpoint(
499
+ critic_path, del_local_after_load=self.config.trainer.del_local_ckpt_after_load
500
+ )
501
+ return self.current_param_version
502
+
503
+ def _collect_metrics_from_samples(self, batch, metrics):
504
+ """
505
+ Collect metrics from samples
506
+ """
507
+ if hasattr(batch, "meta_info") and batch.meta_info:
508
+ samples_param_versions = batch.meta_info["rollout_param_versions"]
509
+ stale_count = sum(1 for v in samples_param_versions if self.current_param_version - v >= 1)
510
+ self.stale_samples_processed += stale_count
511
+ trajectory_param_versions = batch.meta_info["trajectory_param_versions"]
512
+ stale_traj_count = sum(1 for v in trajectory_param_versions if self.current_param_version - v >= 1)
513
+ self.stale_trajectory_processed += stale_traj_count
514
+ metrics.update(
515
+ {
516
+ "fully_async/count/stale_samples_processed": self.stale_samples_processed,
517
+ "fully_async/count/stale_trajectory_processed": self.stale_trajectory_processed,
518
+ "fully_async/count/current_param_version": self.current_param_version,
519
+ }
520
+ )
521
+ for key, value in batch.meta_info.items():
522
+ if key.startswith("fully_async") or key.startswith("timing_s"):
523
+ metrics[key] = value
524
+
525
+ async def _trigger_parameter_sync_after_step(self, validate: bool = False, global_steps: int = None):
526
+ """
527
+ Trigger parameter synchronization after training step
528
+ This ensures rollouter always uses the latest trained parameters
529
+ """
530
+ if self.local_trigger_step < self.trigger_parameter_sync_step and not validate:
531
+ self.local_trigger_step += 1
532
+ return
533
+
534
+ self.current_param_version += 1
535
+ self.local_trigger_step = 1
536
+ self.logger.log(
537
+ data=self.metrics_aggregator.get_aggregated_metrics(),
538
+ step=self.current_param_version,
539
+ )
540
+ self.progress_bar.update(1)
541
+ self.metrics_aggregator.reset()
542
+ timing_param_sync = {}
543
+ with marked_timer("timing_s/wait_last_valid", timing_param_sync):
544
+ ray.get(self.param_synchronizer.wait_last_valid.remote())
545
+ with marked_timer("timing_s/param_sync", timing_param_sync):
546
+ ray.get(
547
+ self.param_synchronizer.sync_weights.remote(
548
+ self.current_param_version,
549
+ validate=validate,
550
+ global_steps=global_steps,
551
+ use_trainer_do_validate=self.config.async_training.use_trainer_do_validate,
552
+ )
553
+ )
554
+
555
+ # do trainer validate
556
+ do_validate_param = (
557
+ self.config.rollout.test_freq > 0
558
+ and self.current_param_version % self.config.rollout.test_freq == 0
559
+ and self.current_param_version > 0
560
+ )
561
+ print(f"do_validate_param: {do_validate_param}")
562
+ if do_validate_param and self.reward_fn is not None and self.config.async_training.use_trainer_do_validate:
563
+ print(f"[FullyAsyncTrainer] validate param version: {self.current_param_version}")
564
+ await self._validate_process()
565
+ else:
566
+ self.train_val_metrics = None
567
+ self.logger.log(data=timing_param_sync, step=self.current_param_version)
568
+
569
+ def _log_validation_data(self):
570
+ """
571
+ Log validation data
572
+ """
573
+ val_data = self.message_queue_client.get_validate_sync()
574
+ if not val_data:
575
+ return
576
+
577
+ val_metrics: ValidateMetrics = ray.cloudpickle.loads(val_data)
578
+ if self.train_val_metrics and self.config.async_training.use_trainer_do_validate:
579
+ # merge info
580
+ timing_param_sync = {}
581
+ with marked_timer("timing_s/merge_val", timing_param_sync):
582
+ new_metrics = self._merge_validation_results(self.train_val_metrics, val_metrics.metrics)
583
+ if new_metrics:
584
+ self.logger.log(data=new_metrics, step=val_metrics.param_version)
585
+ pprint(
586
+ f"[FullyAsyncTrainer] parameter version: {val_metrics.param_version} "
587
+ f"Validation metrics: {new_metrics}, timing_param_sync: {timing_param_sync['timing_s/merge_val']}"
588
+ )
589
+ self.logger.log(data=val_metrics.timing_raw, step=val_metrics.param_version)
590
+ else:
591
+ if val_metrics.metrics:
592
+ self.logger.log(data=val_metrics.metrics, step=val_metrics.param_version)
593
+ pprint(
594
+ f"[FullyAsyncTrainer] parameter version: {val_metrics.param_version} "
595
+ f"Validation metrics: {val_metrics.metrics}"
596
+ )
597
+ self.logger.log(data=val_metrics.timing_raw, step=val_metrics.param_version)
598
+
599
+ async def _validate_process(self):
600
+ if self.config.async_training.use_trainer_do_validate:
601
+ print("[FullyAsyncTrainer] _validate_process")
602
+ from verl.utils.profiler import marked_timer
603
+
604
+ timing_raw = {}
605
+ await self.async_rollout_manager.wake_up()
606
+ with marked_timer("trainer/validate_time", timing_raw):
607
+ self.train_val_metrics = self._validate(True)
608
+ await self.async_rollout_manager.sleep()
609
+ print(f"[FullyAsyncTrainer] validate timing_raw validate: {timing_raw['trainer/validate_time']}")
610
+ else:
611
+ self.train_val_metrics = None
612
+ print("[FullyAsyncTrainer] _validate_process without async_rollout_manager")
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_utils.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import torch
17
+ from megatron.core.distributed import DistributedDataParallel as DDP
18
+
19
+
20
+ @torch.no_grad()
21
+ def copy_megatron_model_to_cpu(models):
22
+ """
23
+ Copy Megatron model parameters to CPU memory (non-destructive copy).
24
+ Unlike offload_megatron_model_to_cpu which moves data, this function creates
25
+ independent copies on CPU while keeping GPU data intact.
26
+
27
+ Args:
28
+ models: List of model chunks (DDP-wrapped or unwrapped)
29
+
30
+ Returns:
31
+ dict: CPU state containing copied parameters and buffers
32
+ """
33
+ cpu_state = {}
34
+
35
+ for model_idx, model_chunk in enumerate(models):
36
+ if isinstance(model_chunk, DDP):
37
+ # Handle DDP-wrapped models
38
+ model_chunk_all_buffers = [model_chunk.buffers, model_chunk.expert_parallel_buffers]
39
+ buffer_states = []
40
+
41
+ for buffers in model_chunk_all_buffers:
42
+ buffer_list = []
43
+ for buffer in buffers:
44
+ buffer_state = {}
45
+
46
+ # Copy parameter data to CPU
47
+ if buffer.param_data.storage().size() > 0:
48
+ buffer_state["param_data"] = buffer.param_data.data.cpu().clone().pin_memory()
49
+
50
+ buffer_list.append(buffer_state)
51
+ buffer_states.append(buffer_list)
52
+
53
+ cpu_state[f"model_chunk_{model_idx}"] = {"buffer_states": buffer_states, "is_ddp": True}
54
+ else:
55
+ # Handle non-DDP models (ref module)
56
+ model_state = {}
57
+ for name, param in model_chunk.named_parameters():
58
+ param_state = {"data": param.data.cpu().clone().pin_memory()}
59
+ model_state[name] = param_state
60
+
61
+ cpu_state[f"model_chunk_{model_idx}"] = {"model_state": model_state, "is_ddp": False}
62
+
63
+ return cpu_state
64
+
65
+
66
+ @torch.no_grad()
67
+ def restore_megatron_model_from_cpu(models, cpu_state):
68
+ """
69
+ Restore Megatron model parameters from CPU memory back to GPU.
70
+
71
+ Args:
72
+ models: List of model chunks to restore to
73
+ cpu_state: CPU state dict returned from copy_megatron_model_to_cpu
74
+ """
75
+ for model_idx, model_chunk in enumerate(models):
76
+ chunk_key = f"model_chunk_{model_idx}"
77
+ if chunk_key not in cpu_state:
78
+ continue
79
+
80
+ chunk_state = cpu_state[chunk_key]
81
+
82
+ if chunk_state["is_ddp"] and isinstance(model_chunk, DDP):
83
+ # Restore DDP buffers
84
+ model_chunk_all_buffers = [model_chunk.buffers, model_chunk.expert_parallel_buffers]
85
+ buffer_states = chunk_state["buffer_states"]
86
+
87
+ for buffers, buffer_list in zip(model_chunk_all_buffers, buffer_states, strict=False):
88
+ for buffer, buffer_state in zip(buffers, buffer_list, strict=False):
89
+ # Restore parameter data
90
+ if "param_data" in buffer_state:
91
+ buffer.param_data.data.copy_(buffer_state["param_data"].to(buffer.param_data.device))
92
+
93
+ elif not chunk_state["is_ddp"] and not isinstance(model_chunk, DDP):
94
+ # Restore non-DDP models
95
+ model_state = chunk_state["model_state"]
96
+ for name, param in model_chunk.named_parameters():
97
+ if name in model_state:
98
+ param_state = model_state[name]
99
+ param.data.copy_(param_state["data"].to(param.device))
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_worker.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
3
+ # Copyright 2025 NVIDIA Ltd. and/or its affiliates
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import logging
18
+ import os
19
+ import time
20
+
21
+ import torch
22
+ import torch.distributed
23
+ from omegaconf import DictConfig
24
+
25
+ from verl.experimental.fully_async_policy.base_detach_sync import BaseDetachNcclSync
26
+ from verl.experimental.fully_async_policy.megatron_utils import (
27
+ copy_megatron_model_to_cpu,
28
+ restore_megatron_model_from_cpu,
29
+ )
30
+ from verl.single_controller.base.decorator import Dispatch, register
31
+ from verl.utils.device import (
32
+ get_device_name,
33
+ get_torch_device,
34
+ )
35
+ from verl.utils.megatron_utils import load_megatron_model_to_gpu, offload_megatron_model_to_cpu, per_tensor_generator
36
+ from verl.workers.megatron_workers import AsyncActorRolloutRefWorker, CriticWorker
37
+
38
+ logger = logging.getLogger(__file__)
39
+ logger.setLevel(os.getenv("VERL_LOGGING_LEVEL", "WARN"))
40
+
41
+ device_name = get_device_name()
42
+
43
+ __all__ = ["DetachActorWorker", "DetachAsyncRolloutWorker", "CriticWorker"]
44
+
45
+
46
+ class DetachNcclSync(BaseDetachNcclSync, AsyncActorRolloutRefWorker):
47
+ def __init__(self, config: DictConfig, role: str):
48
+ BaseDetachNcclSync.__init__(self, config, role)
49
+
50
+ AsyncActorRolloutRefWorker.__init__(self, config, role)
51
+
52
+ def _get_actor_params(self):
53
+ pass
54
+
55
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
56
+ def sync_rollout_weights(self, sync_group_name="actor_rollout"):
57
+ assert (self._is_actor or self._is_rollout) and not self.config.hybrid_engine
58
+ assert hasattr(self, "_weights_info") and self._weights_info is not None
59
+ if self._is_actor and self._is_offload_param:
60
+ load_megatron_model_to_gpu(self.actor_module, False)
61
+ params_generator = self._get_actor_params_generator() if self._is_actor else None
62
+ params = {key: tensor for key, tensor in params_generator} if params_generator is not None else None
63
+
64
+ rollout_name = self.config.rollout.name
65
+ inference_model = None
66
+ if self._is_rollout and (not self._is_actor):
67
+ if rollout_name == "vllm":
68
+ inference_model = BaseDetachNcclSync.get_inference_model(self.rollout)
69
+ from verl.utils.vllm.patch import patch_vllm_moe_model_weight_loader
70
+
71
+ patch_vllm_moe_model_weight_loader(inference_model)
72
+ elif rollout_name == "sglang":
73
+ inference_model = self.rollout._engine
74
+ if inference_model is None:
75
+ print("[sync_rollout_weights] Initialize server adapter engine")
76
+
77
+ async def init_engine():
78
+ if hasattr(self.rollout, "_init_server_adapter"):
79
+ await self.rollout._init_server_adapter()
80
+ else:
81
+ print("[sync_rollout_weights] No _init_server_adapter method found")
82
+ return self.rollout._engine
83
+
84
+ inference_model = self._run_async_safely(init_engine())
85
+ if inference_model is None:
86
+ raise RuntimeError(
87
+ f"Failed to initialize rollout engine. "
88
+ f"rollout type: {type(self.rollout)}, "
89
+ f"has _init_server_adapter: {hasattr(self.rollout, '_init_server_adapter')}"
90
+ )
91
+ else:
92
+ raise NotImplementedError(f"Unknown rollout name: {rollout_name}")
93
+
94
+ if rollout_name == "sglang" and self._is_rollout:
95
+ self._sync_sglang_weights(inference_model, params, sync_group_name)
96
+ else:
97
+ self._sync_vllm_weights(inference_model, params, sync_group_name)
98
+
99
+ if self._is_actor and self._is_offload_param:
100
+ offload_megatron_model_to_cpu(self.actor_module)
101
+ get_torch_device().empty_cache()
102
+
103
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
104
+ def save_model_to_cpu(self, n):
105
+ if not hasattr(self, "cpu_saved_models"):
106
+ self.cpu_saved_models = {}
107
+ self.cpu_saved_models[n] = copy_megatron_model_to_cpu(self.actor.actor_module)
108
+
109
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
110
+ def restore_model_from_cpu(self, n):
111
+ if n in self.cpu_saved_models:
112
+ restore_megatron_model_from_cpu(self.actor.actor_module, self.cpu_saved_models[n])
113
+
114
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
115
+ def clear_cpu_model(self, n):
116
+ if n in self.cpu_saved_models:
117
+ del self.cpu_saved_models[n]
118
+
119
+ def cache_actor_weights_to_cpu(self):
120
+ self.cpu_named_params = {}
121
+ if self._is_actor:
122
+ params_generator = self._get_actor_params_generator()
123
+ local_rank = torch.distributed.get_rank()
124
+ world_size = torch.distributed.get_world_size()
125
+ print(f"cache_actor_weights_to_cpu, local_rank:{local_rank}, world_size:{world_size}")
126
+ for tensor_idx, (key, tensor) in enumerate(params_generator):
127
+ if tensor_idx % world_size == local_rank:
128
+ self.cpu_named_params[key] = tensor.to("cpu", non_blocking=True)
129
+ get_torch_device().synchronize()
130
+
131
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL, blocking=False)
132
+ def sync_rollout_weights_by_checkpoint(self, sync_group_name="actor_rollout"):
133
+ assert (self._is_actor or self._is_rollout) and not self.config.hybrid_engine
134
+ assert hasattr(self, "_weights_info") and self._weights_info is not None
135
+
136
+ # Load model to GPU
137
+ load_start_time = time.time()
138
+ if self._is_actor and self._is_offload_param:
139
+ load_megatron_model_to_gpu(self.actor_module, False)
140
+ load_duration = time.time() - load_start_time
141
+
142
+ from ray.util.collective import collective
143
+
144
+ # Cache actor weights to CPU and measure the time taken
145
+ cache_start_time = time.time()
146
+ self.cache_actor_weights_to_cpu()
147
+ cache_end_time = time.time()
148
+ cache_duration = cache_end_time - cache_start_time
149
+
150
+ # Register the cached weights into the checkpoint engine
151
+ self.checkpoint_engine.register_checkpoint(self._weights_info, self.cpu_named_params)
152
+ register_end_time = time.time()
153
+ register_duration = register_end_time - cache_end_time
154
+ self.cpu_named_params = {}
155
+
156
+ collective.barrier(group_name=sync_group_name)
157
+ update_start_time = time.time()
158
+
159
+ rollout_name = self.config.rollout.name
160
+ inference_model = None
161
+ if self._is_rollout and (not self._is_actor):
162
+ if rollout_name == "vllm":
163
+ inference_model = BaseDetachNcclSync.get_inference_model(self.rollout)
164
+ from verl.utils.vllm.patch import patch_vllm_moe_model_weight_loader
165
+
166
+ patch_vllm_moe_model_weight_loader(inference_model)
167
+ elif rollout_name == "sglang":
168
+ inference_model = self.rollout._engine
169
+ # For ServerAdapter, _engine might be None and needs async initialization
170
+ if inference_model is None:
171
+ # Initialize the server adapter engine
172
+ print("[sync_rollout_weights] Initialize server adapter engine")
173
+
174
+ async def init_engine():
175
+ if hasattr(self.rollout, "_init_server_adapter"):
176
+ await self.rollout._init_server_adapter()
177
+ else:
178
+ print("[sync_rollout_weights] No _init_server_adapter method found")
179
+ return self.rollout._engine
180
+
181
+ inference_model = self._run_async_safely(init_engine())
182
+ if inference_model is None:
183
+ raise RuntimeError(
184
+ f"Failed to initialize rollout engine. "
185
+ f"rollout type: {type(self.rollout)}, "
186
+ f"has _init_server_adapter: {hasattr(self.rollout, '_init_server_adapter')}"
187
+ )
188
+ else:
189
+ raise NotImplementedError(f"Unknown rollout name: {rollout_name}")
190
+ # Update the checkpoint with the inference model and broadcast weights
191
+ self.checkpoint_engine.update_checkpoint(
192
+ inference_model=inference_model,
193
+ group_name=sync_group_name,
194
+ overlap_broadcast_and_consume=self.config.checkpoint_engine.overlap_broadcast_and_consume,
195
+ )
196
+
197
+ update_end_time = time.time()
198
+ update_duration = update_end_time - update_start_time
199
+
200
+ collective.barrier(group_name=sync_group_name)
201
+ offload_start_time = time.time()
202
+ if self._is_actor and self._is_offload_param:
203
+ offload_megatron_model_to_cpu(self.actor_module)
204
+ offload_duration = time.time() - offload_start_time
205
+
206
+ print(
207
+ f"sync_rollout_weights_by_checkpoint finish!, rank:{torch.distributed.get_rank()},"
208
+ f" is_actor:{self._is_actor}, is_rollout:{self._is_rollout},"
209
+ f" total cost:{update_end_time - cache_start_time} seconds, while cache cost {cache_duration} seconds, "
210
+ f" register cost {register_duration} seconds, update cost {update_duration} seconds"
211
+ )
212
+
213
+ if self._is_actor and self._is_offload_param:
214
+ print(
215
+ f"sync_rollout_weights_by_checkpoint load model to gpu cost {load_duration} seconds,"
216
+ f" offload model to cpu cost {offload_duration} seconds"
217
+ )
218
+
219
+
220
+ class DetachActorWorker(DetachNcclSync):
221
+ def __init__(self, config: DictConfig, role: str):
222
+ print("[DetachAsyncRolloutWorker] Initializing via DetachNcclSync...")
223
+ DetachNcclSync.__init__(self, config, role)
224
+
225
+ def _get_actor_params_generator(self):
226
+ assert self._is_actor
227
+ if self.bridge is not None:
228
+ generator = self.bridge.export_weights(self.actor.actor_module)
229
+ else:
230
+ generator = per_tensor_generator(
231
+ self.actor.actor_module,
232
+ self.actor_model_config,
233
+ self.weight_converter,
234
+ self.tf_config,
235
+ self.layer_name_mapping,
236
+ )
237
+
238
+ return generator
239
+
240
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
241
+ def get_actor_weights_info(self):
242
+ assert self._is_actor
243
+ if hasattr(self, "_weights_info"):
244
+ return self._weights_info
245
+ if self._is_offload_param:
246
+ load_megatron_model_to_gpu(self.actor_module, False)
247
+ params_generator = self._get_actor_params_generator()
248
+ ret = []
249
+ for key, tensor in params_generator:
250
+ ret.append((key, tensor.size(), tensor.dtype))
251
+
252
+ self._weights_info = ret
253
+ # Here, we only call this function at the beginning,
254
+ # and immediately afterwards we call sync_rollout_weights.
255
+ # So we no longer call offload in this.
256
+ return ret
257
+
258
+
259
+ class DetachAsyncRolloutWorker(DetachNcclSync):
260
+ def __init__(self, config: DictConfig, role: str):
261
+ print(f"[DetachAsyncRolloutWorker] {DetachAsyncRolloutWorker.__mro__}")
262
+ DetachNcclSync.__init__(self, config, role)
263
+
264
+ @register(dispatch_mode=Dispatch.ONE_TO_ALL)
265
+ def set_actor_weights_info(self, weights_info):
266
+ assert self._is_rollout
267
+ self._weights_info = weights_info
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/message_queue.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import logging
17
+ from collections import deque
18
+ from typing import Any
19
+
20
+ import ray
21
+ from omegaconf import DictConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @ray.remote(num_cpus=2, max_concurrency=20)
27
+ class MessageQueue:
28
+ """
29
+ Simplified Ray-based asynchronous message queue for communication between Rollouter and Trainer
30
+ """
31
+
32
+ def __init__(self, config: DictConfig, max_queue_size: int = 1000):
33
+ self.config = config
34
+ if max_queue_size is None:
35
+ raise ValueError(f"max_queue_size cannot be None, got: {max_queue_size}")
36
+ self.max_queue_size = int(max_queue_size)
37
+ self.queue = deque(maxlen=self.max_queue_size)
38
+ self.current_param_version = 0
39
+
40
+ self.val_queue = deque()
41
+
42
+ try:
43
+ if hasattr(config, "async_training") and config.async_training is not None:
44
+ self.staleness_threshold = getattr(config.async_training, "staleness_threshold", 3)
45
+ else:
46
+ self.staleness_threshold = 3
47
+ except (AttributeError, RecursionError):
48
+ self.staleness_threshold = 3
49
+
50
+ # Asyncio for message handling
51
+ self.running = True
52
+
53
+ # async safe
54
+ self._lock = asyncio.Lock()
55
+ self._consumer_condition = asyncio.Condition(self._lock)
56
+
57
+ # statistic message
58
+ self.total_produced = 0
59
+ self.total_consumed = 0
60
+ self.dropped_samples = 0
61
+
62
+ print(
63
+ f"[MessageQueue] initialized with max_queue_size={max_queue_size},"
64
+ f"staleness_threshold={self.staleness_threshold}"
65
+ )
66
+
67
+ async def put_sample(self, sample: Any, param_version: int) -> bool:
68
+ """
69
+ Put a batch sample into the queue
70
+
71
+ Args:
72
+ sample: Sample data
73
+ param_version: Parameter version number
74
+
75
+ Returns:
76
+ bool: Whether the sample was successfully put into the queue
77
+ """
78
+ async with self._lock:
79
+ # If queue is full, remove the oldest sample (rarely happens)
80
+ is_drop = False
81
+ if len(self.queue) >= self.max_queue_size:
82
+ self.queue.popleft()
83
+ self.dropped_samples += 1
84
+ is_drop = True
85
+ logger.warning("Queue full, dropped sample")
86
+ self.queue.append(sample)
87
+ self.total_produced += 1
88
+
89
+ # Notify waiting consumers
90
+ self._consumer_condition.notify_all()
91
+
92
+ if self.total_produced % 100 == 0:
93
+ print(f"MessageQueue stats: produced={self.total_produced}, queue_size={len(self.queue)}")
94
+ if is_drop:
95
+ return False
96
+ return True
97
+
98
+ async def get_sample(self) -> Any | None:
99
+ """
100
+ Get a single sample from the queue, wait until one is available
101
+
102
+ Returns:
103
+ Any: Single sample data or None if queue is closed
104
+ """
105
+ async with self._lock:
106
+ while len(self.queue) == 0 and self.running:
107
+ await self._consumer_condition.wait()
108
+
109
+ # If queue is closed and empty, return None
110
+ if not self.running and len(self.queue) == 0:
111
+ return None
112
+
113
+ # Get one sample
114
+ data = self.queue.popleft()
115
+ self.total_consumed += 1
116
+ return data, len(self.queue)
117
+
118
+ async def update_param_version(self, version: int):
119
+ """Update current parameter version"""
120
+ async with self._lock:
121
+ old_version = self.current_param_version
122
+ self.current_param_version = version
123
+ print(f"Parameter version updated from {old_version} to {version}")
124
+
125
+ async def get_queue_size(self) -> int:
126
+ """Get current queue length"""
127
+ async with self._lock:
128
+ return len(self.queue)
129
+
130
+ async def get_statistics(self) -> dict[str, Any]:
131
+ """Get queue statistics"""
132
+ async with self._lock:
133
+ return {
134
+ "queue_size": len(self.queue),
135
+ "total_produced": self.total_produced,
136
+ "total_consumed": self.total_consumed,
137
+ "dropped_samples": self.dropped_samples,
138
+ "current_param_version": self.current_param_version,
139
+ "staleness_threshold": self.staleness_threshold,
140
+ "max_queue_size": self.max_queue_size,
141
+ }
142
+
143
+ async def clear_queue(self):
144
+ """Clear the queue"""
145
+ async with self._lock:
146
+ cleared_count = len(self.queue)
147
+ self.queue.clear()
148
+ logger.info(f"Cleared {cleared_count} samples from queue")
149
+
150
+ async def shutdown(self):
151
+ """Shutdown the message queue"""
152
+ async with self._lock:
153
+ self.running = False
154
+ # Notify all waiting coroutines so they can exit
155
+ self._consumer_condition.notify_all()
156
+ logger.info("MessageQueue shutdown")
157
+
158
+ async def get_memory_usage(self) -> dict:
159
+ """Get memory usage statistics"""
160
+ async with self._lock:
161
+ # Estimate memory usage of samples in queue
162
+ import sys
163
+
164
+ total_size = 0
165
+ sample_count = len(self.queue)
166
+
167
+ if sample_count > 0:
168
+ # Estimate size of a single sample (simplified estimation)
169
+ sample = list(self.queue)[0]
170
+ try:
171
+ sample_size = sys.getsizeof(sample)
172
+ # Since we now store RolloutSample directly, estimate based on its components
173
+ if hasattr(sample, "original_batch_dict") and sample.original_batch_dict:
174
+ # Estimate batch data size
175
+ batch_data = sample.original_batch_dict.get("batch", {})
176
+ sample_size += len(batch_data) * 1000 # Roughly estimate 1KB per batch entry
177
+ if hasattr(sample, "agent_loop_output"):
178
+ # Estimate AgentLoopOutput size
179
+ sample_size += 5000 # Roughly estimate 5KB for AgentLoopOutput
180
+ total_size = sample_size * sample_count
181
+ except Exception:
182
+ total_size = sample_count * 15000 # Roughly estimate 15KB per RolloutSample
183
+
184
+ return {
185
+ "queue_samples": sample_count,
186
+ "estimated_memory_bytes": total_size,
187
+ "estimated_memory_mb": total_size / (1024 * 1024),
188
+ }
189
+
190
+ async def put_validate(self, data):
191
+ async with self._lock:
192
+ self.val_queue.append(data)
193
+
194
+ async def get_validate(self):
195
+ async with self._lock:
196
+ if self.val_queue:
197
+ return self.val_queue.popleft()
198
+ else:
199
+ return None
200
+
201
+
202
+ class MessageQueueClient:
203
+ """Asyncio-compatible MessageQueue client for communicating with MessageQueue Actor"""
204
+
205
+ def __init__(self, queue_actor: Any):
206
+ self.queue_actor = queue_actor
207
+
208
+ async def put_sample(self, sample: Any, param_version: int) -> bool:
209
+ """Put batch into queue (async)"""
210
+ future = self.queue_actor.put_sample.remote(sample, param_version)
211
+ return await asyncio.wrap_future(future.future())
212
+
213
+ async def put_validate(self, data: Any) -> bool:
214
+ future = self.queue_actor.put_validate.remote(data)
215
+ return await asyncio.wrap_future(future.future())
216
+
217
+ def get_validate_sync(self) -> Any | None:
218
+ return ray.get(self.queue_actor.get_validate.remote())
219
+
220
+ async def get_sample(self) -> Any | None:
221
+ """Get single sample from queue, wait until one is available (async)"""
222
+ future = self.queue_actor.get_sample.remote()
223
+ return await asyncio.wrap_future(future.future())
224
+
225
+ async def get_queue_size(self) -> int:
226
+ """Get queue size (async)"""
227
+ future = self.queue_actor.get_queue_size.remote()
228
+ return await asyncio.wrap_future(future.future())
229
+
230
+ async def get_statistics(self) -> dict[str, Any]:
231
+ """Get statistics (async)"""
232
+ future = self.queue_actor.get_statistics.remote()
233
+ return await asyncio.wrap_future(future.future())
234
+
235
+ async def clear_queue(self):
236
+ """Clear queue (async)"""
237
+ future = self.queue_actor.clear_queue.remote()
238
+ await asyncio.wrap_future(future.future())
239
+
240
+ async def shutdown(self):
241
+ """Shutdown queue (async)"""
242
+ future = self.queue_actor.shutdown.remote()
243
+ await asyncio.wrap_future(future.future())
244
+
245
+ async def get_memory_usage(self) -> dict:
246
+ """Get memory usage statistics (async)"""
247
+ future = self.queue_actor.get_memory_usage.remote()
248
+ return await asyncio.wrap_future(future.future())
249
+
250
+ # Synchronous version of the method (deprecated)
251
+ def put_sample_sync(self, sample: Any, param_version: int) -> bool:
252
+ """Put batch into queue (sync - deprecated, use put_sample instead)"""
253
+ return ray.get(self.queue_actor.put_sample.remote(sample, param_version))
254
+
255
+ def get_sample_sync(self) -> Any | None:
256
+ """Get single sample from queue (sync - deprecated, use get_sample instead)"""
257
+ return ray.get(self.queue_actor.get_sample.remote())
258
+
259
+ def get_statistics_sync(self) -> dict[str, Any]:
260
+ """Get statistics (sync - deprecated, use get_statistics instead)"""
261
+ return ray.get(self.queue_actor.get_statistics.remote())
262
+
263
+ def update_param_version_sync(self, version: int):
264
+ """Update parameter version (async)"""
265
+ return ray.get(self.queue_actor.update_param_version.remote(version))
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/param_sync.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ import time
17
+
18
+ import ray
19
+ from ray.util.collective import collective
20
+
21
+ from verl.utils.device import get_nccl_backend
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @ray.remote
27
+ class ParameterSynchronizer:
28
+ """
29
+ Unified parameter synchronizer, responsible for synchronizing model parameters between actor and rollout
30
+ Based on the mature synchronization mode implementation of one_step_off_policy
31
+ Merges the functions of the original multiple synchronizer classes
32
+ """
33
+
34
+ def __init__(self, config, trainer, rollouter, mq):
35
+ self.config = config
36
+ self.trainer = trainer
37
+ self.rollouter = rollouter
38
+ self.mq_client = mq
39
+ self.actor_wg = ray.get(trainer.get_actor_wg.remote())
40
+ self.rollout_wg = ray.get(rollouter.get_rollout_wg.remote())
41
+
42
+ # Basic attributes
43
+ self.weights_info = None
44
+ self.sync_group_initialized = False
45
+ self.sync_group_name = "actor_rollout"
46
+ self.wait_last_update = None
47
+ self.wait_last_resume = None
48
+ self.validate_task = None
49
+
50
+ # Statistics
51
+ self.current_version = 0
52
+
53
+ self._init_weights_info()
54
+ self._init_sync_group()
55
+
56
+ if self.config.async_training.checkpoint_engine.enable:
57
+ self._init_actor_rollout_checkpoint_engine()
58
+
59
+ def get_current_param_version(self) -> int:
60
+ """Get current parameter version number"""
61
+ return self.current_version
62
+
63
+ def get_weights_info(self):
64
+ """Get weights info"""
65
+ return self.weights_info
66
+
67
+ def _init_weights_info(self):
68
+ self.weights_info = self.actor_wg.get_actor_weights_info()[0]
69
+ self.rollout_wg.set_actor_weights_info(self.weights_info)
70
+
71
+ def _init_sync_group(self):
72
+ print("[ParameterSynchronizer] Initializing parameter synchronization group...")
73
+ actor_rollout_workers = self.actor_wg.workers + self.rollout_wg.workers
74
+ n_workers = len(self.actor_wg.workers + self.rollout_wg.workers)
75
+ if self.config.trainer.device == "npu":
76
+ master_address = ray.get(self.actor_wg.workers[0]._get_node_ip.remote()).strip("[]")
77
+ master_port = ray.get(self.actor_wg.workers[0]._get_free_port.remote())
78
+ self.actor_wg.create_weight_sync_group(
79
+ master_address,
80
+ master_port,
81
+ 0,
82
+ n_workers,
83
+ )
84
+ ray.get(
85
+ self.rollout_wg.create_weight_sync_group(
86
+ master_address,
87
+ master_port,
88
+ len(self.actor_wg.workers),
89
+ n_workers,
90
+ )
91
+ )
92
+ else:
93
+ collective.create_collective_group(
94
+ actor_rollout_workers,
95
+ n_workers,
96
+ list(range(0, n_workers)),
97
+ backend=get_nccl_backend(),
98
+ group_name=self.sync_group_name,
99
+ )
100
+
101
+ def _init_actor_rollout_checkpoint_engine(self):
102
+ ray.get(
103
+ self.actor_wg.init_checkpoint_engine(
104
+ rank_offset=0,
105
+ actor_num=len(self.actor_wg.workers),
106
+ rollout_num=len(self.rollout_wg.workers),
107
+ )
108
+ )
109
+ ray.get(
110
+ self.rollout_wg.init_checkpoint_engine(
111
+ rank_offset=len(self.actor_wg.workers),
112
+ actor_num=len(self.actor_wg.workers),
113
+ rollout_num=len(self.rollout_wg.workers),
114
+ )
115
+ )
116
+
117
+ def sync_weights(self, version, validate=False, global_steps=0, use_trainer_do_validate=False):
118
+ """Sync weights between trainer and rollouter, and update parameter version"""
119
+ start_time = time.time()
120
+
121
+ self.current_version = version
122
+ ray.get(self.rollouter.pause.remote())
123
+
124
+ print(f"[ParameterSynchronizer] rollout paused. cost {time.time() - start_time:.2f} seconds")
125
+ # Update MQ version
126
+ self.mq_client.update_param_version_sync(version)
127
+
128
+ pause_time = time.time()
129
+
130
+ # sync weights
131
+ # For sglang, always use sync_rollout_weights instead of sync_rollout_weights_by_checkpoint
132
+ rollout_name = getattr(self.config.actor_rollout_ref.rollout, "name", None)
133
+ use_checkpoint_engine = self.config.async_training.checkpoint_engine.enable and rollout_name != "sglang"
134
+
135
+ if use_checkpoint_engine:
136
+ self.actor_wg.sync_rollout_weights_by_checkpoint(self.sync_group_name)
137
+ ray.get(self.rollout_wg.sync_rollout_weights_by_checkpoint(self.sync_group_name))
138
+ else:
139
+ self.actor_wg.sync_rollout_weights(self.sync_group_name)
140
+ ray.get(self.rollout_wg.sync_rollout_weights(self.sync_group_name))
141
+ end_time = time.time()
142
+ print(
143
+ f"[ParameterSynchronizer] sync_weights success. cost {end_time - start_time:.2f} seconds, "
144
+ f"pause:{pause_time - start_time:.2f}s, sync:{end_time - pause_time:.2f}s"
145
+ )
146
+ # async train do validate
147
+ print(f"[ParameterSynchronizer] validate: {validate}, use_trainer_do_validate: {use_trainer_do_validate}")
148
+ if validate and use_trainer_do_validate:
149
+ print("[ParameterSynchronizer] use trainer to do validate")
150
+ self.validate_task = self.trainer._validate_process.remote()
151
+ else:
152
+ self.validate_task = None
153
+ # Async Update rollout version & validation
154
+ self.wait_last_update = self.rollouter.update_param_version.remote(
155
+ version, validate, global_steps, use_trainer_do_validate
156
+ )
157
+ self.wait_last_resume = self.rollouter.resume.remote(self.wait_last_update)
158
+
159
+ def wait_last_valid(self):
160
+ print("[ParameterSynchronizer] Waiting last sync and validate...")
161
+ start_time = time.time()
162
+ if self.wait_last_update:
163
+ ray.get(self.wait_last_update)
164
+ if self.wait_last_resume:
165
+ ray.get(self.wait_last_resume)
166
+ if self.validate_task:
167
+ ray.get(self.validate_task)
168
+ print(f"[ParameterSynchronizer] Wait last validate cost: {time.time() - start_time:.2f} seconds")
169
+
170
+ def rollouter_save_checkpoint(self, local_global_step_folder: str):
171
+ """Trigger rollout to save checkpoint(dataloader)"""
172
+ print(f"[ParameterSynchronizer] Triggering checkpoint save at {local_global_step_folder} ...")
173
+ return ray.get(self.rollouter.save_checkpoint.remote(local_global_step_folder))
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/ray_trainer.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Bytedance Ltd. and/or its affiliates
2
+ # Copyright 2023-2024 SGLang Team
3
+ # Copyright 2025 ModelBest Inc. and/or its affiliates
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ PPO Trainer with Ray-based single controller.
18
+ This trainer supports model-agonistic model initialization with huggingface
19
+ """
20
+
21
+ import uuid
22
+ from copy import deepcopy
23
+ from pprint import pprint
24
+
25
+ import numpy as np
26
+ import ray
27
+ import torch
28
+ from omegaconf import OmegaConf
29
+ from tqdm import tqdm
30
+
31
+ from verl import DataProto
32
+ from verl.experimental.dataset.sampler import AbstractCurriculumSampler
33
+ from verl.single_controller.ray import RayClassWithInitArgs
34
+ from verl.single_controller.ray.base import create_colocated_worker_cls
35
+ from verl.trainer.ppo.core_algos import AdvantageEstimator, agg_loss
36
+ from verl.trainer.ppo.metric_utils import (
37
+ compute_data_metrics,
38
+ compute_throughout_metrics,
39
+ compute_timing_metrics,
40
+ )
41
+ from verl.trainer.ppo.ray_trainer import RayPPOTrainer, apply_kl_penalty, compute_advantage, compute_response_mask
42
+ from verl.trainer.ppo.reward import compute_reward, compute_reward_async
43
+ from verl.trainer.ppo.utils import Role
44
+ from verl.utils.config import omega_conf_to_dataclass
45
+ from verl.utils.debug import marked_timer
46
+ from verl.utils.metric import (
47
+ reduce_metrics,
48
+ )
49
+ from verl.utils.rollout_skip import RolloutSkip
50
+
51
+
52
+ class FullyAsyncRayPPOTrainer(RayPPOTrainer):
53
+ def init_workers(self):
54
+ """Initialize distributed training workers using Ray backend.
55
+
56
+ Creates:
57
+ 1. Ray resource pools from configuration
58
+ 2. Worker groups for each role (actor, critic, etc.)
59
+ """
60
+ self._init_resource_pools()
61
+ self._create_worker_classes()
62
+ self._init_worker_groups()
63
+ self._init_models()
64
+ self._init_async_rollout_manager()
65
+
66
+ def _init_resource_pools(self):
67
+ self.resource_pool_manager.create_resource_pool()
68
+
69
+ self.resource_pool_to_cls = {pool: {} for pool in self.resource_pool_manager.resource_pool_dict.values()}
70
+
71
+ def _create_worker_classes(self):
72
+ self._create_actor_rollout_classes()
73
+ self._create_critic_class()
74
+ self._create_reference_policy_class()
75
+ self._create_reward_model_class()
76
+
77
+ def _create_actor_rollout_classes(self):
78
+ raise NotImplementedError
79
+
80
+ def _create_critic_class(self):
81
+ # create critic
82
+ if self.use_critic:
83
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.Critic)
84
+ critic_cfg = omega_conf_to_dataclass(self.config.critic)
85
+ critic_cls = RayClassWithInitArgs(cls=self.role_worker_mapping[Role.Critic], config=critic_cfg)
86
+ self.resource_pool_to_cls[resource_pool][str(Role.Critic)] = critic_cls
87
+
88
+ def _create_reference_policy_class(self):
89
+ # create reference policy if needed
90
+ if self.use_reference_policy:
91
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RefPolicy)
92
+ ref_policy_cls = RayClassWithInitArgs(
93
+ self.role_worker_mapping[Role.RefPolicy],
94
+ config=self.config.actor_rollout_ref,
95
+ role=str(Role.RefPolicy),
96
+ # profile_option=self.config.trainer.npu_profile.options,
97
+ )
98
+ self.resource_pool_to_cls[resource_pool][str(Role.RefPolicy)] = ref_policy_cls
99
+
100
+ def _create_reward_model_class(self):
101
+ # create a reward model if reward_fn is None
102
+ if self.use_rm:
103
+ # we create a RM here
104
+ resource_pool = self.resource_pool_manager.get_resource_pool(Role.RewardModel)
105
+ rm_cls = RayClassWithInitArgs(self.role_worker_mapping[Role.RewardModel], config=self.config.reward_model)
106
+ self.resource_pool_to_cls[resource_pool][str(Role.RewardModel)] = rm_cls
107
+
108
+ def _init_worker_groups(self):
109
+ # initialize WorkerGroup
110
+ # NOTE: if you want to use a different resource pool for each role, which can support different parallel size,
111
+ # you should not use `create_colocated_worker_cls`.
112
+ # Instead, directly pass different resource pool to different worker groups.
113
+ # See https://github.com/volcengine/verl/blob/master/examples/ray/tutorial.ipynb for more information.
114
+ all_wg = {}
115
+ wg_kwargs = {} # Setting up kwargs for RayWorkerGroup
116
+ if OmegaConf.select(self.config.trainer, "ray_wait_register_center_timeout") is not None:
117
+ wg_kwargs["ray_wait_register_center_timeout"] = self.config.trainer.ray_wait_register_center_timeout
118
+ if OmegaConf.select(self.config.global_profiler, "steps") is not None:
119
+ wg_kwargs["profile_steps"] = OmegaConf.select(self.config.global_profiler, "steps")
120
+ # Only require nsight worker options when tool is nsys
121
+ if OmegaConf.select(self.config.global_profiler, "tool") == "nsys":
122
+ assert (
123
+ OmegaConf.select(self.config.global_profiler.global_tool_config.nsys, "worker_nsight_options")
124
+ is not None
125
+ ), "worker_nsight_options must be set when using nsys with profile_steps"
126
+ wg_kwargs["worker_nsight_options"] = OmegaConf.to_container(
127
+ OmegaConf.select(self.config.global_profiler.global_tool_config.nsys, "worker_nsight_options")
128
+ )
129
+ wg_kwargs["device_name"] = self.device_name
130
+
131
+ for resource_pool, class_dict in self.resource_pool_to_cls.items():
132
+ worker_dict_cls = create_colocated_worker_cls(class_dict=class_dict)
133
+ wg_dict = self.ray_worker_group_cls(
134
+ resource_pool=resource_pool,
135
+ ray_cls_with_init=worker_dict_cls,
136
+ **wg_kwargs,
137
+ )
138
+ spawn_wg = wg_dict.spawn(prefix_set=class_dict.keys())
139
+ all_wg.update(spawn_wg)
140
+ self.all_wg = all_wg
141
+
142
+ def _init_models(self):
143
+ if self.use_critic:
144
+ self.critic_wg = self.all_wg[str(Role.Critic)]
145
+ self.critic_wg.init_model()
146
+
147
+ if self.use_reference_policy and not self.ref_in_actor:
148
+ self.ref_policy_wg = self.all_wg[str(Role.RefPolicy)]
149
+ self.ref_policy_wg.init_model()
150
+
151
+ if self.use_rm:
152
+ self.rm_wg = self.all_wg[str(Role.RewardModel)]
153
+ self.rm_wg.init_model()
154
+
155
+ # we should create rollout at the end so that vllm can have a better estimation of kv cache memory
156
+ self.actor_rollout_wg = self.all_wg[str(Role.ActorRollout)]
157
+ self.actor_rollout_wg.init_model()
158
+
159
+ def _init_async_rollout_manager(self):
160
+ pass
161
+
162
+ def fit(self):
163
+ """
164
+ The training loop of PPO.
165
+ The driver process only need to call the compute functions of the worker group through RPC
166
+ to construct the PPO dataflow.
167
+ The light-weight advantage computation is done on the driver process.
168
+ """
169
+ from omegaconf import OmegaConf
170
+
171
+ from verl.utils.tracking import Tracking
172
+
173
+ logger = Tracking(
174
+ project_name=self.config.trainer.project_name,
175
+ experiment_name=self.config.trainer.experiment_name,
176
+ default_backend=self.config.trainer.logger,
177
+ config=OmegaConf.to_container(self.config, resolve=True),
178
+ )
179
+
180
+ self.global_steps = 0
181
+
182
+ # load checkpoint before doing anything
183
+ self._load_checkpoint()
184
+
185
+ # perform validation before training
186
+ # currently, we only support validation using the reward_function.
187
+ if self.val_reward_fn is not None and self.config.trainer.get("val_before_train", True):
188
+ val_metrics = self._validate()
189
+ assert val_metrics, f"{val_metrics=}"
190
+ pprint(f"Initial validation metrics: {val_metrics}")
191
+ logger.log(data=val_metrics, step=self.global_steps)
192
+ if self.config.trainer.get("val_only", False):
193
+ return
194
+
195
+ if self.config.actor_rollout_ref.rollout.get("skip_rollout", False):
196
+ rollout_skip = RolloutSkip(self.config, self.actor_rollout_wg)
197
+ rollout_skip.wrap_generate_sequences()
198
+
199
+ # add tqdm
200
+ progress_bar = tqdm(total=self.total_training_steps, initial=self.global_steps, desc="Training Progress")
201
+
202
+ # we start from step 1
203
+ self.global_steps += 1
204
+ last_val_metrics = None
205
+ self.max_steps_duration = 0
206
+
207
+ prev_step_profile = False
208
+ curr_step_profile = (
209
+ self.global_steps in self.config.global_profiler.steps
210
+ if self.config.global_profiler.steps is not None
211
+ else False
212
+ )
213
+ next_step_profile = False
214
+
215
+ for epoch in range(self.config.trainer.total_epochs):
216
+ for batch_dict in self.train_dataloader:
217
+ metrics = {}
218
+ timing_raw = {}
219
+
220
+ with marked_timer("start_profile", timing_raw):
221
+ self._start_profiling(
222
+ not prev_step_profile and curr_step_profile
223
+ if self.config.global_profiler.profile_continuous_steps
224
+ else curr_step_profile
225
+ )
226
+
227
+ batch, gen_batch = self._prepare_generate_batch(batch_dict)
228
+
229
+ is_last_step = self.global_steps >= self.total_training_steps
230
+
231
+ with marked_timer("step", timing_raw):
232
+ # generate a batch
233
+ with marked_timer("gen", timing_raw, color="red"):
234
+ if not self.async_rollout_mode:
235
+ gen_batch_output = self.actor_rollout_wg.generate_sequences(gen_batch)
236
+ else:
237
+ gen_batch_output = self.async_rollout_manager.generate_sequences(gen_batch)
238
+ timing_raw.update(gen_batch_output.meta_info["timing"])
239
+ gen_batch_output.meta_info.pop("timing", None)
240
+
241
+ if self.config.algorithm.adv_estimator == AdvantageEstimator.REMAX:
242
+ if self.reward_fn is None:
243
+ raise ValueError("A reward_fn is required for REMAX advantage estimation.")
244
+
245
+ with marked_timer("gen_max", timing_raw, color="purple"):
246
+ gen_baseline_batch = deepcopy(gen_batch)
247
+ gen_baseline_batch.meta_info["do_sample"] = False
248
+ if not self.async_rollout_mode:
249
+ gen_baseline_output = self.actor_rollout_wg.generate_sequences(gen_baseline_batch)
250
+ else:
251
+ gen_baseline_output = self.async_rollout_manager.generate_sequences(gen_baseline_batch)
252
+ batch = batch.union(gen_baseline_output)
253
+ reward_baseline_tensor = self.reward_fn(batch)
254
+ reward_baseline_tensor = reward_baseline_tensor.sum(dim=-1)
255
+
256
+ batch.pop(batch_keys=list(gen_baseline_output.batch.keys()))
257
+
258
+ batch.batch["reward_baselines"] = reward_baseline_tensor
259
+
260
+ del gen_baseline_batch, gen_baseline_output
261
+
262
+ batch = self._post_generate_batch(batch, gen_batch_output, metrics)
263
+ batch, reward_extra_infos_dict = self._process_batch_common(batch, metrics, timing_raw)
264
+ self._log_rollout(batch, reward_extra_infos_dict, timing_raw)
265
+
266
+ last_val_metrics = self._validate_metrics(is_last_step, last_val_metrics, metrics, timing_raw)
267
+ self._check_save_checkpoint(is_last_step, timing_raw)
268
+
269
+ with marked_timer("stop_profile", timing_raw):
270
+ next_step_profile = (
271
+ self.global_steps + 1 in self.config.global_profiler.steps
272
+ if self.config.global_profiler.steps is not None
273
+ else False
274
+ )
275
+ self._stop_profiling(
276
+ curr_step_profile and not next_step_profile
277
+ if self.config.global_profiler.profile_continuous_steps
278
+ else curr_step_profile
279
+ )
280
+ prev_step_profile = curr_step_profile
281
+ curr_step_profile = next_step_profile
282
+
283
+ self._collect_metrics(batch, epoch, metrics, timing_raw)
284
+ self._post_batch_processing(batch)
285
+
286
+ # TODO: make a canonical logger that supports various backend
287
+ logger.log(data=metrics, step=self.global_steps)
288
+
289
+ progress_bar.update(1)
290
+ self.global_steps += 1
291
+
292
+ if (
293
+ hasattr(self.config.actor_rollout_ref.actor, "profiler")
294
+ and self.config.actor_rollout_ref.actor.profiler.tool == "torch_memory"
295
+ ):
296
+ self.actor_rollout_wg.dump_memory_snapshot(
297
+ tag=f"post_update_step{self.global_steps}", sub_dir=f"step{self.global_steps}"
298
+ )
299
+
300
+ if is_last_step:
301
+ pprint(f"Final validation metrics: {last_val_metrics}")
302
+ progress_bar.close()
303
+ return
304
+
305
+ def _prepare_generate_batch(self, batch_dict):
306
+ batch: DataProto = DataProto.from_single_dict(batch_dict)
307
+
308
+ # add uid to batch
309
+ batch.non_tensor_batch["uid"] = np.array([str(uuid.uuid4()) for _ in range(len(batch.batch))], dtype=object)
310
+
311
+ gen_batch = self._get_gen_batch(batch)
312
+
313
+ # pass global_steps to trace
314
+ gen_batch.meta_info["global_steps"] = self.global_steps
315
+ gen_batch = gen_batch.repeat(repeat_times=self.config.actor_rollout_ref.rollout.n, interleave=True)
316
+ return batch, gen_batch
317
+
318
+ def _post_generate_batch(self, batch, gen_batch_output, metrics):
319
+ # repeat to align with repeated responses in rollout
320
+ batch = batch.repeat(repeat_times=self.config.actor_rollout_ref.rollout.n, interleave=True)
321
+ batch = batch.union(gen_batch_output)
322
+
323
+ if "response_mask" not in batch.batch.keys():
324
+ batch.batch["response_mask"] = compute_response_mask(batch)
325
+ # Balance the number of valid tokens across DP ranks.
326
+ # NOTE: This usually changes the order of data in the `batch`,
327
+ # which won't affect the advantage calculation (since it's based on uid),
328
+ # but might affect the loss calculation (due to the change of mini-batching).
329
+ # TODO: Decouple the DP balancing and mini-batching.
330
+ if self.config.trainer.balance_batch:
331
+ self._balance_batch(batch, metrics=metrics)
332
+
333
+ # compute global_valid tokens
334
+ batch.meta_info["global_token_num"] = torch.sum(batch.batch["attention_mask"], dim=-1).tolist()
335
+
336
+ return batch
337
+
338
+ def _process_batch_common(self, batch, metrics, timing_raw, local_trigger_step=None):
339
+ with marked_timer("reward", timing_raw, color="yellow"):
340
+ # compute reward model score
341
+ if self.use_rm:
342
+ reward_tensor = self.rm_wg.compute_rm_score(batch)
343
+ batch = batch.union(reward_tensor)
344
+
345
+ if self.config.reward_model.launch_reward_fn_async:
346
+ future_reward = compute_reward_async.remote(data=batch, reward_fn=self.reward_fn)
347
+ else:
348
+ reward_tensor, reward_extra_infos_dict = compute_reward(batch, self.reward_fn)
349
+
350
+ with marked_timer("old_log_prob", timing_raw, color="blue"):
351
+
352
+ def compute_old_log_prob(batch):
353
+ old_log_prob = self.actor_rollout_wg.compute_log_prob(batch)
354
+ entropys = old_log_prob.batch["entropys"]
355
+ response_masks = batch.batch["response_mask"]
356
+ actor_config = self.config.actor_rollout_ref.actor
357
+ entropy_agg = agg_loss(
358
+ loss_mat=entropys,
359
+ loss_mask=response_masks,
360
+ loss_agg_mode=actor_config.loss_agg_mode,
361
+ loss_scale_factor=actor_config.loss_scale_factor,
362
+ )
363
+ old_log_prob_metrics = {"actor/entropy": entropy_agg.detach().item()}
364
+ metrics.update(old_log_prob_metrics)
365
+ old_log_prob.batch.pop("entropys")
366
+ batch = batch.union(old_log_prob)
367
+ if "rollout_log_probs" in batch.batch.keys():
368
+ # TODO: we may want to add diff of probs too.
369
+ from verl.utils.debug.metrics import calculate_debug_metrics
370
+
371
+ metrics.update(calculate_debug_metrics(batch))
372
+ return batch
373
+
374
+ async_training = self.config.get("async_training", None)
375
+ if async_training and async_training.use_rollout_log_probs:
376
+ # If local_triger_step == 1, load the training engine's parameters to the CPU
377
+ # and save a copy for subsequent MIS use.
378
+ # If local_trigger_step == 2, 3, ..., restore the parameters of version 1 to calculate the old_log_prob,
379
+ # then restore the parameters of the current version.
380
+ if local_trigger_step == 1:
381
+ self.actor_rollout_wg.save_model_to_cpu(1)
382
+ batch = compute_old_log_prob(batch)
383
+ elif local_trigger_step is not None:
384
+ self.actor_rollout_wg.save_model_to_cpu(local_trigger_step)
385
+ self.actor_rollout_wg.restore_model_from_cpu(1)
386
+ batch = compute_old_log_prob(batch)
387
+ self.actor_rollout_wg.restore_model_from_cpu(local_trigger_step)
388
+ self.actor_rollout_wg.clear_cpu_model(local_trigger_step)
389
+ else:
390
+ batch.batch["old_log_probs"] = batch.batch["rollout_log_probs"]
391
+ batch.meta_info["temperature"] = self.config.actor_rollout_ref.rollout.temperature
392
+
393
+ else:
394
+ batch = compute_old_log_prob(batch)
395
+
396
+ if self.use_reference_policy:
397
+ # compute reference log_prob
398
+ with marked_timer("ref", timing_raw, color="olive"):
399
+ if not self.ref_in_actor:
400
+ ref_log_prob = self.ref_policy_wg.compute_ref_log_prob(batch)
401
+ else:
402
+ ref_log_prob = self.actor_rollout_wg.compute_ref_log_prob(batch)
403
+ batch = batch.union(ref_log_prob)
404
+
405
+ # compute values
406
+ if self.use_critic:
407
+ with marked_timer("values", timing_raw, color="cyan"):
408
+ values = self.critic_wg.compute_values(batch)
409
+ batch = batch.union(values)
410
+
411
+ with marked_timer("adv", timing_raw, color="brown"):
412
+ # we combine with rule-based rm
413
+ reward_extra_infos_dict: dict[str, list]
414
+ if self.config.reward_model.launch_reward_fn_async:
415
+ reward_tensor, reward_extra_infos_dict = ray.get(future_reward)
416
+ batch.batch["token_level_scores"] = reward_tensor
417
+
418
+ if reward_extra_infos_dict:
419
+ batch.non_tensor_batch.update({k: np.array(v) for k, v in reward_extra_infos_dict.items()})
420
+
421
+ # compute rewards. apply_kl_penalty if available
422
+ if self.config.algorithm.use_kl_in_reward:
423
+ batch, kl_metrics = apply_kl_penalty(
424
+ batch, kl_ctrl=self.kl_ctrl_in_reward, kl_penalty=self.config.algorithm.kl_penalty
425
+ )
426
+ metrics.update(kl_metrics)
427
+ else:
428
+ batch.batch["token_level_rewards"] = batch.batch["token_level_scores"]
429
+
430
+ # Compute rollout correction weights centrally (once per batch)
431
+ # This corrects for off-policy issues (policy mismatch, model staleness, etc.)
432
+ # Also computes off-policy diagnostic metrics (KL, PPL, etc.)
433
+ from verl.trainer.ppo.rollout_corr_helper import compute_rollout_correction_and_add_to_batch
434
+
435
+ rollout_corr_config = self.config.algorithm.get("rollout_correction", None)
436
+ if rollout_corr_config is not None and "rollout_log_probs" in batch.batch:
437
+ batch, is_metrics = compute_rollout_correction_and_add_to_batch(batch, rollout_corr_config)
438
+ # IS and off-policy metrics already have rollout_corr/ prefix
439
+ metrics.update(is_metrics)
440
+
441
+ # compute advantages, executed on the driver process
442
+ norm_adv_by_std_in_grpo = self.config.algorithm.get(
443
+ "norm_adv_by_std_in_grpo", True
444
+ ) # GRPO adv normalization factor
445
+
446
+ batch = compute_advantage(
447
+ batch,
448
+ adv_estimator=self.config.algorithm.adv_estimator,
449
+ gamma=self.config.algorithm.gamma,
450
+ lam=self.config.algorithm.lam,
451
+ num_repeat=self.config.actor_rollout_ref.rollout.n,
452
+ norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,
453
+ config=self.config.algorithm,
454
+ )
455
+
456
+ # update critic
457
+ if self.use_critic:
458
+ with marked_timer("update_critic", timing_raw, color="pink"):
459
+ critic_output = self.critic_wg.update_critic(batch)
460
+ critic_output_metrics = reduce_metrics(critic_output.meta_info["metrics"])
461
+ metrics.update(critic_output_metrics)
462
+
463
+ # implement critic warmup
464
+ if self.config.trainer.critic_warmup <= self.global_steps:
465
+ # update actor
466
+ with marked_timer("update_actor", timing_raw, color="red"):
467
+ batch.meta_info["multi_turn"] = self.config.actor_rollout_ref.rollout.multi_turn.enable
468
+ actor_output = self.actor_rollout_wg.update_actor(batch)
469
+ actor_output_metrics = reduce_metrics(actor_output.meta_info["metrics"])
470
+ metrics.update(actor_output_metrics)
471
+ return batch, reward_extra_infos_dict
472
+
473
+ def _log_rollout(self, batch, reward_extra_infos_dict, timing_raw):
474
+ # Log rollout generations if enabled
475
+ rollout_data_dir = self.config.trainer.get("rollout_data_dir", None)
476
+ if rollout_data_dir:
477
+ with marked_timer("dump_rollout_generations", timing_raw, color="green"):
478
+ inputs = self.tokenizer.batch_decode(batch.batch["prompts"], skip_special_tokens=True)
479
+ outputs = self.tokenizer.batch_decode(batch.batch["responses"], skip_special_tokens=True)
480
+ scores = batch.batch["token_level_scores"].sum(-1).cpu().tolist()
481
+ sample_gts = [item.non_tensor_batch.get("reward_model", {}).get("ground_truth", None) for item in batch]
482
+
483
+ if "request_id" in batch.non_tensor_batch:
484
+ reward_extra_infos_dict.setdefault(
485
+ "request_id",
486
+ batch.non_tensor_batch["request_id"].tolist(),
487
+ )
488
+
489
+ self._dump_generations(
490
+ inputs=inputs,
491
+ outputs=outputs,
492
+ gts=sample_gts,
493
+ scores=scores,
494
+ reward_extra_infos_dict=reward_extra_infos_dict,
495
+ dump_path=rollout_data_dir,
496
+ )
497
+
498
+ def _validate_metrics(self, is_last_step, last_val_metrics, metrics, timing_raw):
499
+ if (
500
+ self.val_reward_fn is not None
501
+ and self.config.trainer.test_freq > 0
502
+ and (is_last_step or self.global_steps % self.config.trainer.test_freq == 0)
503
+ ):
504
+ with marked_timer("testing", timing_raw, color="green"):
505
+ val_metrics: dict = self._validate()
506
+ if is_last_step:
507
+ last_val_metrics = val_metrics
508
+ metrics.update(val_metrics)
509
+ return last_val_metrics
510
+
511
+ def _collect_metrics(self, batch, epoch, metrics, timing_raw):
512
+ steps_duration = timing_raw["step"]
513
+ self.max_steps_duration = max(self.max_steps_duration, steps_duration)
514
+
515
+ # training metrics
516
+ metrics.update(
517
+ {
518
+ "training/global_step": self.global_steps,
519
+ "training/epoch": epoch,
520
+ }
521
+ )
522
+ # collect metrics
523
+ metrics.update(compute_data_metrics(batch=batch, use_critic=self.use_critic))
524
+ metrics.update(compute_timing_metrics(batch=batch, timing_raw=timing_raw))
525
+ # TODO: implement actual tflpo and theoretical tflpo
526
+ n_gpus = self.resource_pool_manager.get_n_gpus()
527
+ metrics.update(compute_throughout_metrics(batch=batch, timing_raw=timing_raw, n_gpus=n_gpus))
528
+
529
+ def _post_batch_processing(self, batch: DataProto):
530
+ # this is experimental and may be changed/removed in the future in favor of a general-purpose one
531
+ if isinstance(self.train_dataloader.sampler, AbstractCurriculumSampler):
532
+ self.train_dataloader.sampler.update(batch=batch)
533
+
534
+ # this is experimental and may be changed/removed in the future
535
+ # in favor of a general-purpose data buffer pool
536
+ if hasattr(self.train_dataset, "on_batch_end"):
537
+ # The dataset may be changed after each training batch
538
+ self.train_dataset.on_batch_end(batch=batch)
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/sglang_async_server.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import logging
16
+ from typing import Any, Optional
17
+
18
+ import ray
19
+ import torch
20
+ from ray.actor import ActorHandle
21
+
22
+ from verl.workers.config import HFModelConfig, RewardModelConfig, RolloutConfig
23
+ from verl.workers.rollout.replica import RolloutMode
24
+ from verl.workers.rollout.sglang_rollout.async_sglang_server import (
25
+ SGLangHttpServer,
26
+ SGLangReplica,
27
+ )
28
+
29
+ logger = logging.getLogger(__file__)
30
+ logger.setLevel(logging.INFO)
31
+
32
+
33
+ class SGLangHttpServerForPartial(SGLangHttpServer):
34
+ def __init__(
35
+ self,
36
+ config: RolloutConfig | RewardModelConfig,
37
+ model_config: HFModelConfig,
38
+ rollout_mode: RolloutMode,
39
+ workers: list[ActorHandle],
40
+ replica_rank: int,
41
+ node_rank: int,
42
+ nnodes: int,
43
+ cuda_visible_devices: str,
44
+ base_gpu_id: int,
45
+ ):
46
+ super().__init__(
47
+ config=config,
48
+ model_config=model_config,
49
+ rollout_mode=rollout_mode,
50
+ workers=workers,
51
+ replica_rank=replica_rank,
52
+ node_rank=node_rank,
53
+ nnodes=nnodes,
54
+ cuda_visible_devices=cuda_visible_devices,
55
+ base_gpu_id=base_gpu_id,
56
+ )
57
+
58
+ # for cancel LLMServer
59
+ self.paused = False
60
+ self.lock = asyncio.Lock()
61
+ self.cancel_event: dict[str, asyncio.Event] = {}
62
+ self.req_output: dict[str, Optional[dict[str, Any]]] = {}
63
+
64
+ async def _generate_step(
65
+ self,
66
+ prompt_ids: torch.Tensor,
67
+ sampling_params: dict[str, Any],
68
+ request_id: str,
69
+ image_data: Optional[list[Any]] = None,
70
+ ) -> None:
71
+ sampling_params = dict(sampling_params)
72
+
73
+ max_new_tokens = min(
74
+ self.config.response_length,
75
+ self.config.max_model_len - len(prompt_ids) - 1,
76
+ )
77
+ sampling_params["max_new_tokens"] = max_new_tokens
78
+
79
+ sampling_params.setdefault(
80
+ "repetition_penalty",
81
+ self.config.get("repetition_penalty", 1.0),
82
+ )
83
+
84
+ sampling_params.pop("logprobs", None)
85
+ return_logprob = True
86
+ from sglang.srt.managers.io_struct import GenerateReqInput
87
+
88
+ request = GenerateReqInput(
89
+ rid=request_id,
90
+ input_ids=prompt_ids,
91
+ sampling_params=sampling_params,
92
+ return_logprob=return_logprob,
93
+ image_data=image_data,
94
+ )
95
+ generator = self.tokenizer_manager.generate_request(request, None)
96
+ async for output in generator:
97
+ self.req_output[request_id] = output
98
+
99
+ assert self.req_output[request_id] is not None
100
+
101
+ async def generate_for_partial(
102
+ self,
103
+ prompt_ids: torch.Tensor,
104
+ sampling_params: dict[str, Any],
105
+ request_id: str,
106
+ image_data: Optional[list[Any]] = None,
107
+ ) -> tuple[list[int], list[float], bool]:
108
+ async with self.lock:
109
+ if self.paused:
110
+ return [], [], True
111
+ self.req_output[request_id] = None
112
+ self.cancel_event[request_id] = asyncio.Event()
113
+ cancel_handle = asyncio.create_task(self.cancel_event[request_id].wait())
114
+ generation_handle = asyncio.create_task(
115
+ self._generate_step(prompt_ids, sampling_params, request_id, image_data)
116
+ )
117
+ done, pending = await asyncio.wait(
118
+ [generation_handle, cancel_handle],
119
+ return_when=asyncio.FIRST_COMPLETED,
120
+ )
121
+ for task in done:
122
+ await task
123
+
124
+ for task in pending:
125
+ task.cancel()
126
+ async with self.lock:
127
+ output = self.req_output.get(request_id)
128
+ if output is None:
129
+ self.cancel_event.pop(request_id, None)
130
+ self.req_output.pop(request_id, None)
131
+ return [], [], True
132
+ meta_info = output.get("meta_info", {})
133
+ output_token_logprobs = meta_info.get("output_token_logprobs")
134
+
135
+ token_ids: list[int] = []
136
+ log_probs: list[float] = []
137
+
138
+ if output_token_logprobs is not None:
139
+ for log_prob, token_id, _ in output_token_logprobs:
140
+ token_ids.append(int(token_id))
141
+ log_probs.append(float(log_prob))
142
+ else:
143
+ token_ids = list(output["output_ids"])
144
+ log_probs = []
145
+ is_cancel = generation_handle not in done
146
+ self.cancel_event.pop(request_id, None)
147
+ self.req_output.pop(request_id, None)
148
+
149
+ return token_ids, log_probs, is_cancel
150
+
151
+ async def cancel(self):
152
+ async with self.lock:
153
+ self.paused = True
154
+ for request_id in self.cancel_event:
155
+ self.cancel_event[request_id].set()
156
+
157
+ async def resume(self):
158
+ async with self.lock:
159
+ self.paused = False
160
+
161
+ async def reset_prefix_cache(self):
162
+ async with self.lock:
163
+ print("Reset prefix cache ...")
164
+ await self.tokenizer_manager.flush_cache()
165
+
166
+
167
+ class FullyAsyncSGLangReplica(SGLangReplica):
168
+ def __init__(
169
+ self,
170
+ replica_rank: int,
171
+ config: RolloutConfig | RewardModelConfig,
172
+ model_config: HFModelConfig,
173
+ gpus_per_node: int = 8,
174
+ is_reward_model: bool = False,
175
+ ):
176
+ super().__init__(replica_rank, config, model_config, gpus_per_node, is_reward_model)
177
+ self.server_class = ray.remote(SGLangHttpServerForPartial)
178
+
179
+ async def cancel(self):
180
+ """Cancel each rollout server."""
181
+ await asyncio.gather(*[server.cancel.remote() for server in self.servers])
182
+
183
+ async def resume(self):
184
+ """Resume each rollout server."""
185
+ await asyncio.gather(*[server.resume.remote() for server in self.servers])
186
+
187
+ async def reset_prefix_cache(self):
188
+ """reset kv cache in each rollout server."""
189
+ await asyncio.gather(*[server.reset_prefix_cache.remote() for server in self.servers])
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_30b_a3b_base_math_fsdp.sh ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO-Qwen3-30B-A3B-Base-Async'
5
+ exp_name='Fsdp2-tp4sp4'
6
+
7
+ # Ray
8
+ RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ DATA_PATH=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ DATA_PATH=${DATA_PATH:-"/mnt/bn/${BYTENAS}"}
14
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
15
+ MODEL_PATH=${MODEL_PATH:-"${DATA_PATH}/shared/models/Qwen3-30B-A3B-Base"}
16
+ CKPTS_DIR=${CKPTS_DIR:-"${DATA_PATH}/ckpts/${project_name}/${exp_name}"}
17
+ TRAIN_FILE=${TRAIN_FILE:-"${DATA_PATH}/shared/data/dapo-math/dapo-math-17k.parquet"}
18
+ TEST_FILE=${TEST_FILE:-"${DATA_PATH}/shared/data/dapo-math/aime-2024.parquet"}
19
+
20
+
21
+ rollout_mode="async"
22
+ rollout_name="vllm" # sglang or vllm
23
+ if [ "$rollout_mode" = "async" ]; then
24
+ export VLLM_USE_V1=1
25
+ return_raw_chat="True"
26
+ fi
27
+
28
+ # Algorithm parameters
29
+ adv_estimator=grpo
30
+
31
+ use_kl_in_reward=False
32
+ kl_coef=0.0
33
+ use_kl_loss=False
34
+ kl_loss_coef=0.0
35
+
36
+ clip_ratio_low=0.2
37
+ clip_ratio_high=0.28
38
+
39
+ # Response length parameters
40
+ max_prompt_length=$((1024 * 2))
41
+ max_response_length=$((1024 * 20))
42
+ enable_overlong_buffer=True
43
+ overlong_buffer_len=$((1024 * 4))
44
+ overlong_penalty_factor=1.0
45
+
46
+ # Training parameters
47
+ loss_agg_mode="token-mean"
48
+ enable_filter_groups=True
49
+ filter_groups_metric=acc
50
+ max_num_gen_batches=10
51
+
52
+ # Algorithm
53
+ temperature=1.0
54
+ top_p=1.0
55
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
56
+ val_top_p=0.7
57
+
58
+
59
+ NNODES=${NNODES:-4}
60
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
61
+
62
+ # Fully async specific parameters
63
+ n_gpus_rollout=8
64
+ n_gpus_training=8
65
+ n_nodes_rollout=2
66
+ n_nodes_train=2 # $((NNODES - n_nodes_rollout))
67
+
68
+ train_bsz=512
69
+ train_prompt_bsz=0
70
+ gen_prompt_bsz=1
71
+ n_resp_per_prompt=16
72
+ train_prompt_mini_bsz=32
73
+ total_rollout_steps=$(((train_bsz * 400)))
74
+ test_freq=25
75
+ staleness_threshold=0.6 # 0 0.3 1
76
+ require_batches=1
77
+ total_train_gpus=$((n_gpus_training * n_nodes_train))
78
+ total_rollout_gpus=$((n_gpus_rollout * n_nodes_rollout))
79
+ trigger_parameter_sync_step=$((train_bsz / ( train_prompt_mini_bsz * require_batches))) # 8 16 32
80
+ partial_rollout=True
81
+ enforce_eager=False
82
+ nccl_timeout=72000
83
+ enable_sleep_mode=False
84
+
85
+ # Performance Related Parameter
86
+ sp_size=4
87
+ use_dynamic_bsz=True
88
+ actor_ppo_max_token_len=$((max_prompt_length + max_response_length))
89
+ infer_ppo_max_token_len=$((max_prompt_length + max_response_length))
90
+ ref_offload=True
91
+ actor_offload=False
92
+ gen_tp=4
93
+ fsdp_size=-1
94
+
95
+
96
+ ray job submit --no-wait --runtime-env="${RUNTIME_ENV}" \
97
+ --working-dir "${WORKING_DIR}" \
98
+ --address "${RAY_ADDRESS}" \
99
+ -- python3 -m verl.experimental.fully_async_policy.fully_async_main \
100
+ --config-path=config \
101
+ --config-name='fully_async_dapo_trainer.yaml' \
102
+ data.train_files="${TRAIN_FILE}" \
103
+ data.val_files="${TEST_FILE}" \
104
+ data.prompt_key=prompt \
105
+ data.truncation='left' \
106
+ actor_rollout_ref.actor.strategy=fsdp \
107
+ critic.strategy=fsdp \
108
+ data.max_prompt_length=${max_prompt_length} \
109
+ data.max_response_length=${max_response_length} \
110
+ data.train_batch_size=${train_prompt_bsz} \
111
+ data.gen_batch_size=${gen_prompt_bsz} \
112
+ data.return_raw_chat=${return_raw_chat} \
113
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
114
+ algorithm.adv_estimator=${adv_estimator} \
115
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
116
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
117
+ actor_rollout_ref.rollout.calculate_log_probs=True \
118
+ actor_rollout_ref.nccl_timeout=${nccl_timeout} \
119
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
120
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
121
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
122
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
123
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
124
+ actor_rollout_ref.model.use_remove_padding=True \
125
+ actor_rollout_ref.hybrid_engine=False \
126
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
127
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
128
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
129
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
130
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
131
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
132
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
133
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
134
+ actor_rollout_ref.actor.optim.lr=1e-6 \
135
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
136
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
137
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
138
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
139
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
140
+ actor_rollout_ref.actor.entropy_coeff=0 \
141
+ actor_rollout_ref.actor.grad_clip=1.0 \
142
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
143
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
144
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.50 \
145
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
146
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
147
+ +actor_rollout_ref.rollout.enable_sleep_mode=${enable_sleep_mode} \
148
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
149
+ actor_rollout_ref.rollout.enforce_eager=${enforce_eager} \
150
+ actor_rollout_ref.rollout.temperature=${temperature} \
151
+ actor_rollout_ref.rollout.top_p=${top_p} \
152
+ actor_rollout_ref.rollout.top_k=${top_k} \
153
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
154
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
155
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
156
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
157
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
158
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
159
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
160
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
161
+ actor_rollout_ref.rollout.name=${rollout_name} \
162
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
163
+ reward_model.reward_manager=dapo \
164
+ reward_model.overlong_buffer.enable=${enable_overlong_buffer} \
165
+ reward_model.overlong_buffer.len=${overlong_buffer_len} \
166
+ reward_model.overlong_buffer.penalty_factor=${overlong_penalty_factor} \
167
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
168
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
169
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
170
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
171
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
172
+ trainer.logger=['console','wandb'] \
173
+ trainer.project_name="${project_name}" \
174
+ trainer.experiment_name="${exp_name}-i${total_rollout_gpus}_t${total_train_gpus}_s${staleness_threshold}" \
175
+ trainer.val_before_train=True \
176
+ trainer.test_freq="${test_freq}" \
177
+ trainer.save_freq=-1 \
178
+ trainer.default_local_dir="${CKPTS_DIR}" \
179
+ trainer.resume_mode=auto \
180
+ trainer.nnodes="${n_nodes_train}" \
181
+ trainer.n_gpus_per_node="${n_gpus_training}" \
182
+ rollout.nnodes="${n_nodes_rollout}" \
183
+ rollout.n_gpus_per_node="${n_gpus_rollout}" \
184
+ rollout.total_rollout_steps="${total_rollout_steps}" \
185
+ rollout.test_freq=${test_freq} \
186
+ rollout.total_epochs=10 \
187
+ async_training.require_batches=${require_batches} \
188
+ async_training.staleness_threshold="${staleness_threshold}" \
189
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
190
+ async_training.partial_rollout="${partial_rollout}" \
191
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_async_retool.sh ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ set -x
2
+
3
+ export VLLM_USE_V1=1
4
+
5
+ # ================= data/model/tool =================
6
+ HDFS_ROOT=${HDFS_ROOT:-$PWD}
7
+ DATA_ROOT=${DATA_ROOT:-$PWD}
8
+
9
+ dapo_math_17k=$DATA_ROOT/dataset/BytedTsinghua-SIA/DAPO-Math-17k
10
+ aime_2024=$DATA_ROOT/dataset/Maxwell-Jia/AIME_2024
11
+ aime_2025=$DATA_ROOT/dataset/yentinglin/aime_2025
12
+ model_path=$HDFS_ROOT/checkpoint/multiturn-sft-qwen-2.5-7b-instruct/global_step_372
13
+
14
+ train_files="['$dapo_math_17k']"
15
+ test_files="['$aime_2025', '$aime_2024']"
16
+
17
+ # tool
18
+ tool_config_path=recipe/retool/sandbox_fusion_tool_config.yaml
19
+ retool_path=recipe/retool/retool.py
20
+
21
+ # wandb / tensorboard
22
+ project_name=retool
23
+ experiment_name=qwen2.5-7b_dapo_async_tool
24
+ default_local_dir=$DATA_ROOT/checkpoint/$experiment_name
25
+
26
+ # ================= algorithm =================
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ max_turns=16
38
+ max_prompt_length=2048
39
+ max_response_length=16384
40
+ actor_lr=1e-6
41
+
42
+ # ================= perfomance =================
43
+ infer_tp=4 # vllm
44
+ train_sp=4 # train
45
+ fsdp_size=4 # train
46
+ offload=False
47
+
48
+ actor_max_token_len_per_gpu=$(( (max_prompt_length + max_response_length) * 1 ))
49
+ log_prob_max_token_len_per_gpu=$(( actor_max_token_len_per_gpu * 4 ))
50
+
51
+ # ================= async policy =================
52
+ rollout_name="vllm"
53
+ rollout_mode="async"
54
+
55
+ NNODES=${NNODES:-1}
56
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
57
+ n_gpus_rollout=4
58
+ n_gpus_training=$((NGPUS_PER_NODE - n_gpus_rollout))
59
+
60
+ train_batch_size=0
61
+ ppo_mini_batch_size=16
62
+ gen_prompt_bsz=1
63
+ n_resp_per_prompt=16
64
+ n_resp_per_prompt_val=30
65
+ total_rollout_steps=$(((64*250)))
66
+ test_freq=10
67
+ staleness_threshold=0.5
68
+ trigger_parameter_sync_step=4
69
+ require_batches=1
70
+ partial_rollout=True
71
+
72
+ python3 -m verl.experimental.fully_async_policy.fully_async_main \
73
+ algorithm.adv_estimator=$adv_estimator \
74
+ algorithm.use_kl_in_reward=$use_kl_in_reward \
75
+ algorithm.kl_ctrl.kl_coef=$kl_coef \
76
+ data.train_files="$train_files" \
77
+ data.val_files="$test_files" \
78
+ data.return_raw_chat=True \
79
+ data.train_batch_size=$train_batch_size \
80
+ data.max_prompt_length=$max_prompt_length \
81
+ data.max_response_length=$max_response_length \
82
+ data.filter_overlong_prompts=True \
83
+ data.truncation='error' \
84
+ data.custom_cls.path=$retool_path \
85
+ data.custom_cls.name=CustomRLHFDataset \
86
+ custom_reward_function.path=$retool_path \
87
+ custom_reward_function.name=compute_score \
88
+ actor_rollout_ref.hybrid_engine=False \
89
+ actor_rollout_ref.model.path=$model_path \
90
+ actor_rollout_ref.model.use_remove_padding=True \
91
+ actor_rollout_ref.model.enable_gradient_checkpointing=True \
92
+ actor_rollout_ref.actor.use_kl_loss=$use_kl_loss \
93
+ actor_rollout_ref.actor.kl_loss_coef=$kl_loss_coef \
94
+ actor_rollout_ref.actor.clip_ratio_low=$clip_ratio_low \
95
+ actor_rollout_ref.actor.clip_ratio_high=$clip_ratio_high \
96
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
97
+ actor_rollout_ref.actor.optim.lr=$actor_lr \
98
+ actor_rollout_ref.actor.use_dynamic_bsz=True \
99
+ actor_rollout_ref.actor.ppo_mini_batch_size=$ppo_mini_batch_size \
100
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=$actor_max_token_len_per_gpu \
101
+ actor_rollout_ref.actor.strategy=fsdp2 \
102
+ critic.strategy=fsdp2 \
103
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
104
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=$train_sp \
105
+ actor_rollout_ref.actor.fsdp_config.param_offload=$offload \
106
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=$offload \
107
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=$log_prob_max_token_len_per_gpu \
108
+ actor_rollout_ref.rollout.name=vllm \
109
+ actor_rollout_ref.rollout.mode=async \
110
+ actor_rollout_ref.rollout.tensor_model_parallel_size=$infer_tp \
111
+ actor_rollout_ref.rollout.multi_turn.enable=True \
112
+ actor_rollout_ref.rollout.multi_turn.max_user_turns=$max_turns \
113
+ actor_rollout_ref.rollout.multi_turn.max_assistant_turns=$max_turns \
114
+ actor_rollout_ref.rollout.multi_turn.tool_config_path=$tool_config_path \
115
+ actor_rollout_ref.rollout.multi_turn.format=hermes \
116
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.8 \
117
+ actor_rollout_ref.rollout.n=$n_resp_per_prompt \
118
+ actor_rollout_ref.rollout.val_kwargs.top_p=0.6 \
119
+ actor_rollout_ref.rollout.val_kwargs.temperature=1.0 \
120
+ actor_rollout_ref.rollout.val_kwargs.n=$n_resp_per_prompt_val \
121
+ actor_rollout_ref.rollout.calculate_log_probs=True \
122
+ trainer.logger=['console','tensorboard'] \
123
+ trainer.project_name=$project_name \
124
+ trainer.experiment_name=$experiment_name \
125
+ trainer.val_before_train=True \
126
+ trainer.log_val_generations=20 \
127
+ trainer.save_freq=-1 \
128
+ trainer.default_local_dir=$default_local_dir \
129
+ data.gen_batch_size=${gen_prompt_bsz} \
130
+ trainer.nnodes=$NNODES \
131
+ trainer.n_gpus_per_node=$n_gpus_training \
132
+ rollout.nnodes=$NNODES \
133
+ rollout.n_gpus_per_node=$n_gpus_rollout \
134
+ rollout.total_rollout_steps=$total_rollout_steps \
135
+ rollout.total_epochs=10 \
136
+ rollout.test_freq=$test_freq \
137
+ async_training.staleness_threshold=$staleness_threshold \
138
+ async_training.trigger_parameter_sync_step=$trigger_parameter_sync_step \
139
+ async_training.require_batches=$require_batches \
140
+ async_training.partial_rollout=$partial_rollout \
141
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_16_16.sh ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='dapo_qwen2-7B-math_28k_fsdp2_fully-async_16-16'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 28))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=4
60
+ sp_size=4
61
+ fsdp_size=8
62
+
63
+ # Fully async specific parameters
64
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-2}
65
+ NNODES_TRAIN=${NNODES_TRAIN:-2}
66
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
67
+
68
+ train_prompt_bsz=0
69
+ gen_prompt_bsz=1
70
+ n_resp_per_prompt=16
71
+ train_prompt_mini_bsz=32
72
+ total_rollout_steps=$(((512*400)))
73
+ test_freq=20
74
+ staleness_threshold=0.1
75
+ trigger_parameter_sync_step=4
76
+ require_batches=4
77
+ partial_rollout=True
78
+
79
+ python -m verl.experimental.fully_async_policy.fully_async_main \
80
+ data.train_files="${TRAIN_FILE}" \
81
+ data.val_files="${TEST_FILE}" \
82
+ data.prompt_key=prompt \
83
+ data.truncation='left' \
84
+ data.max_prompt_length=${max_prompt_length} \
85
+ data.max_response_length=${max_response_length} \
86
+ data.train_batch_size=${train_prompt_bsz} \
87
+ data.gen_batch_size=${gen_prompt_bsz} \
88
+ data.return_raw_chat=${return_raw_chat} \
89
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
90
+ algorithm.adv_estimator=${adv_estimator} \
91
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
92
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
93
+ actor_rollout_ref.actor.strategy=fsdp2 \
94
+ critic.strategy=fsdp2 \
95
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
96
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
97
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
98
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
99
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
100
+ actor_rollout_ref.model.use_remove_padding=True \
101
+ actor_rollout_ref.hybrid_engine=False \
102
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
103
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
104
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
105
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
107
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
108
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
109
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
110
+ actor_rollout_ref.actor.optim.lr=1e-6 \
111
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
112
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
113
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
114
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
115
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
116
+ actor_rollout_ref.actor.entropy_coeff=0 \
117
+ actor_rollout_ref.actor.grad_clip=1.0 \
118
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
119
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
120
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
121
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
122
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
123
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
124
+ actor_rollout_ref.rollout.temperature=${temperature} \
125
+ actor_rollout_ref.rollout.top_p=${top_p} \
126
+ actor_rollout_ref.rollout.top_k=${top_k} \
127
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
128
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
129
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
130
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
131
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
132
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
133
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
134
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
135
+ actor_rollout_ref.rollout.name=${rollout_name} \
136
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
137
+ actor_rollout_ref.rollout.calculate_log_probs=True \
138
+ reward_model.reward_manager=dapo \
139
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
140
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
143
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
144
+ trainer.logger=['console','tensorboard'] \
145
+ trainer.project_name="${project_name}" \
146
+ trainer.experiment_name="${exp_name}" \
147
+ trainer.val_before_train=True \
148
+ trainer.save_freq=-1 \
149
+ trainer.default_local_dir="${CKPTS_DIR}" \
150
+ trainer.resume_mode=auto \
151
+ trainer.nnodes="${NNODES_TRAIN}" \
152
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
153
+ rollout.nnodes="${NNODES_ROLLOUT}" \
154
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
155
+ rollout.total_rollout_steps="${total_rollout_steps}" \
156
+ rollout.total_epochs=10 \
157
+ rollout.test_freq="${test_freq}" \
158
+ async_training.staleness_threshold="${staleness_threshold}" \
159
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
160
+ async_training.require_batches="${require_batches}" \
161
+ async_training.partial_rollout="${partial_rollout}" \
162
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_32_32.sh ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='dapo_qwen2-7B-math_28k_fsdp2_fully-async_32-32'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 28))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=4
60
+ sp_size=4
61
+ fsdp_size=8
62
+
63
+ # Fully async specific parameters
64
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-4}
65
+ NNODES_TRAIN=${NNODES_TRAIN:-4}
66
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
67
+
68
+ train_prompt_bsz=0
69
+ gen_prompt_bsz=1
70
+ n_resp_per_prompt=16
71
+ train_prompt_mini_bsz=32
72
+ total_rollout_steps=$(((512*400)))
73
+ test_freq=20
74
+ staleness_threshold=0.1
75
+ trigger_parameter_sync_step=4
76
+ require_batches=4
77
+ partial_rollout=True
78
+
79
+ python -m verl.experimental.fully_async_policy.fully_async_main \
80
+ data.train_files="${TRAIN_FILE}" \
81
+ data.val_files="${TEST_FILE}" \
82
+ data.prompt_key=prompt \
83
+ data.truncation='left' \
84
+ data.max_prompt_length=${max_prompt_length} \
85
+ data.max_response_length=${max_response_length} \
86
+ data.train_batch_size=${train_prompt_bsz} \
87
+ data.gen_batch_size=${gen_prompt_bsz} \
88
+ data.return_raw_chat=${return_raw_chat} \
89
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
90
+ algorithm.adv_estimator=${adv_estimator} \
91
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
92
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
93
+ actor_rollout_ref.actor.strategy=fsdp2 \
94
+ critic.strategy=fsdp2 \
95
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
96
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
97
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
98
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
99
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
100
+ actor_rollout_ref.model.use_remove_padding=True \
101
+ actor_rollout_ref.hybrid_engine=False \
102
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
103
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
104
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
105
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
107
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
108
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
109
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
110
+ actor_rollout_ref.actor.optim.lr=1e-6 \
111
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
112
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
113
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
114
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
115
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
116
+ actor_rollout_ref.actor.entropy_coeff=0 \
117
+ actor_rollout_ref.actor.grad_clip=1.0 \
118
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
119
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
120
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
121
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
122
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
123
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
124
+ actor_rollout_ref.rollout.temperature=${temperature} \
125
+ actor_rollout_ref.rollout.top_p=${top_p} \
126
+ actor_rollout_ref.rollout.top_k=${top_k} \
127
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
128
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
129
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
130
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
131
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
132
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
133
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
134
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
135
+ actor_rollout_ref.rollout.name=${rollout_name} \
136
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
137
+ actor_rollout_ref.rollout.calculate_log_probs=True \
138
+ reward_model.reward_manager=dapo \
139
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
140
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
143
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
144
+ trainer.logger=['console','tensorboard'] \
145
+ trainer.project_name="${project_name}" \
146
+ trainer.experiment_name="${exp_name}" \
147
+ trainer.val_before_train=True \
148
+ trainer.save_freq=-1 \
149
+ trainer.default_local_dir="${CKPTS_DIR}" \
150
+ trainer.resume_mode=auto \
151
+ trainer.nnodes="${NNODES_TRAIN}" \
152
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
153
+ rollout.nnodes="${NNODES_ROLLOUT}" \
154
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
155
+ rollout.total_rollout_steps="${total_rollout_steps}" \
156
+ rollout.total_epochs=10 \
157
+ rollout.test_freq="${test_freq}" \
158
+ async_training.staleness_threshold="${staleness_threshold}" \
159
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
160
+ async_training.require_batches="${require_batches}" \
161
+ async_training.partial_rollout="${partial_rollout}" \
162
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_12.sh ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='DAPO-Qwen2.5-7b-MATH-0527a1-fsdp2-fully-async-4-12'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 8))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=1
60
+ sp_size=1
61
+ fsdp_size=2
62
+
63
+ # Fully async specific parameters
64
+ NNODES=${NNODES:-2}
65
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
66
+
67
+ n_gpus_rollout=2
68
+ n_gpus_training=$((NGPUS_PER_NODE - n_gpus_rollout))
69
+
70
+ train_prompt_bsz=0
71
+ gen_prompt_bsz=1
72
+ n_resp_per_prompt=16
73
+ train_prompt_mini_bsz=32
74
+ total_rollout_steps=$(((512*100)))
75
+ test_freq=10
76
+ staleness_threshold=0.1
77
+ trigger_parameter_sync_step=4
78
+ require_batches=4
79
+ partial_rollout=True
80
+
81
+ python -m verl.experimental.fully_async_policy.fully_async_main \
82
+ data.train_files="${TRAIN_FILE}" \
83
+ data.val_files="${TEST_FILE}" \
84
+ data.prompt_key=prompt \
85
+ data.truncation='left' \
86
+ data.max_prompt_length=${max_prompt_length} \
87
+ data.max_response_length=${max_response_length} \
88
+ data.train_batch_size=${train_prompt_bsz} \
89
+ data.gen_batch_size=${gen_prompt_bsz} \
90
+ data.return_raw_chat=${return_raw_chat} \
91
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
92
+ algorithm.adv_estimator=${adv_estimator} \
93
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
94
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
95
+ actor_rollout_ref.actor.strategy=fsdp2 \
96
+ critic.strategy=fsdp2 \
97
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
98
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
99
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
100
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
101
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
102
+ actor_rollout_ref.model.use_remove_padding=True \
103
+ actor_rollout_ref.hybrid_engine=False \
104
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
105
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
107
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
108
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
109
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
110
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
111
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
112
+ actor_rollout_ref.actor.optim.lr=1e-6 \
113
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
114
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
115
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
116
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
117
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
118
+ actor_rollout_ref.actor.entropy_coeff=0 \
119
+ actor_rollout_ref.actor.grad_clip=1.0 \
120
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
121
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
122
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
123
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
124
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
125
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
126
+ actor_rollout_ref.rollout.temperature=${temperature} \
127
+ actor_rollout_ref.rollout.top_p=${top_p} \
128
+ actor_rollout_ref.rollout.top_k=${top_k} \
129
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
130
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
131
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
132
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
133
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
134
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
135
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
136
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
137
+ actor_rollout_ref.rollout.name=${rollout_name} \
138
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
139
+ actor_rollout_ref.rollout.calculate_log_probs=True \
140
+ reward_model.reward_manager=dapo \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
143
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
144
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
145
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
146
+ trainer.logger=['console','tensorboard'] \
147
+ trainer.project_name="${project_name}" \
148
+ trainer.experiment_name="${exp_name}" \
149
+ trainer.val_before_train=True \
150
+ trainer.test_freq="${test_freq}" \
151
+ trainer.save_freq=-1 \
152
+ trainer.default_local_dir="${CKPTS_DIR}" \
153
+ trainer.resume_mode=auto \
154
+ trainer.nnodes="${NNODES}" \
155
+ trainer.n_gpus_per_node="${n_gpus_training}" \
156
+ rollout.nnodes="${NNODES}" \
157
+ rollout.n_gpus_per_node="${n_gpus_rollout}" \
158
+ rollout.total_rollout_steps="${total_rollout_steps}" \
159
+ rollout.total_epochs=10 \
160
+ async_training.staleness_threshold="${staleness_threshold}" \
161
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
162
+ async_training.require_batches="${require_batches}" \
163
+ async_training.partial_rollout="${partial_rollout}" \
164
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_4.sh ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='DAPO-Qwen2.5-7b-MATH-0527a1-fsdp2-fully-async-4-4'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 8))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=1
60
+ sp_size=1
61
+ fsdp_size=2
62
+
63
+ # Fully async specific parameters
64
+ NNODES=${NNODES:-1}
65
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
66
+
67
+ n_gpus_rollout=4
68
+ n_gpus_training=$((NGPUS_PER_NODE - n_gpus_rollout))
69
+
70
+ train_prompt_bsz=0
71
+ gen_prompt_bsz=1
72
+ n_resp_per_prompt=16
73
+ train_prompt_mini_bsz=32
74
+ total_rollout_steps=$(((512*100)))
75
+ test_freq=10
76
+ staleness_threshold=0.1
77
+ trigger_parameter_sync_step=4
78
+ require_batches=4
79
+ partial_rollout=True
80
+
81
+ python -m verl.experimental.fully_async_policy.fully_async_main \
82
+ data.train_files="${TRAIN_FILE}" \
83
+ data.val_files="${TEST_FILE}" \
84
+ data.prompt_key=prompt \
85
+ data.truncation='left' \
86
+ data.max_prompt_length=${max_prompt_length} \
87
+ data.max_response_length=${max_response_length} \
88
+ data.train_batch_size=${train_prompt_bsz} \
89
+ data.gen_batch_size=${gen_prompt_bsz} \
90
+ data.return_raw_chat=${return_raw_chat} \
91
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
92
+ algorithm.adv_estimator=${adv_estimator} \
93
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
94
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
95
+ actor_rollout_ref.actor.strategy=fsdp2 \
96
+ critic.strategy=fsdp2 \
97
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
98
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
99
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
100
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
101
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
102
+ actor_rollout_ref.model.use_remove_padding=True \
103
+ actor_rollout_ref.hybrid_engine=False \
104
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
105
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
107
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
108
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
109
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
110
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
111
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
112
+ actor_rollout_ref.actor.optim.lr=1e-6 \
113
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
114
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
115
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
116
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
117
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
118
+ actor_rollout_ref.actor.entropy_coeff=0 \
119
+ actor_rollout_ref.actor.grad_clip=1.0 \
120
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
121
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
122
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
123
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
124
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
125
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
126
+ actor_rollout_ref.rollout.temperature=${temperature} \
127
+ actor_rollout_ref.rollout.top_p=${top_p} \
128
+ actor_rollout_ref.rollout.top_k=${top_k} \
129
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
130
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
131
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
132
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
133
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
134
+ actor_rollout_ref.rollout.calculate_log_probs=True \
135
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
136
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
137
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
138
+ actor_rollout_ref.rollout.name=${rollout_name} \
139
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
140
+ reward_model.reward_manager=dapo \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
143
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
144
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
145
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
146
+ trainer.logger=['console','tensorboard'] \
147
+ trainer.project_name="${project_name}" \
148
+ trainer.experiment_name="${exp_name}" \
149
+ trainer.val_before_train=False \
150
+ trainer.save_freq=-1 \
151
+ trainer.default_local_dir="${CKPTS_DIR}" \
152
+ trainer.resume_mode=auto \
153
+ trainer.nnodes="${NNODES}" \
154
+ trainer.n_gpus_per_node="${n_gpus_training}" \
155
+ rollout.nnodes="${NNODES}" \
156
+ rollout.n_gpus_per_node="${n_gpus_rollout}" \
157
+ rollout.total_rollout_steps="${total_rollout_steps}" \
158
+ rollout.total_epochs=10 \
159
+ rollout.test_freq="${test_freq}" \
160
+ async_training.staleness_threshold="${staleness_threshold}" \
161
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
162
+ async_training.require_batches="${require_batches}" \
163
+ async_training.partial_rollout="${partial_rollout}" \
164
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64.sh ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='dapo_qwen2-7B-math_28k_fsdp2_fully-async_64-64'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 28))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=4
60
+ sp_size=4
61
+ fsdp_size=8
62
+
63
+ # Fully async specific parameters
64
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-8}
65
+ NNODES_TRAIN=${NNODES_TRAIN:-8}
66
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
67
+
68
+ train_prompt_bsz=0
69
+ gen_prompt_bsz=1
70
+ n_resp_per_prompt=16
71
+ train_prompt_mini_bsz=32
72
+ total_rollout_steps=$(((512*400)))
73
+ test_freq=20
74
+ staleness_threshold=0.5
75
+ trigger_parameter_sync_step=4
76
+ require_batches=4
77
+ partial_rollout=True
78
+
79
+ python -m verl.experimental.fully_async_policy.fully_async_main \
80
+ data.train_files="${TRAIN_FILE}" \
81
+ data.val_files="${TEST_FILE}" \
82
+ data.prompt_key=prompt \
83
+ data.truncation='left' \
84
+ data.max_prompt_length=${max_prompt_length} \
85
+ data.max_response_length=${max_response_length} \
86
+ data.train_batch_size=${train_prompt_bsz} \
87
+ data.gen_batch_size=${gen_prompt_bsz} \
88
+ data.return_raw_chat=${return_raw_chat} \
89
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
90
+ algorithm.adv_estimator=${adv_estimator} \
91
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
92
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
93
+ actor_rollout_ref.actor.strategy=fsdp2 \
94
+ critic.strategy=fsdp2 \
95
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
96
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
97
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
98
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
99
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
100
+ actor_rollout_ref.model.use_remove_padding=True \
101
+ actor_rollout_ref.hybrid_engine=False \
102
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
103
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
104
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
105
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
107
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
108
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
109
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
110
+ actor_rollout_ref.actor.optim.lr=1e-6 \
111
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
112
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
113
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
114
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
115
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
116
+ actor_rollout_ref.actor.entropy_coeff=0 \
117
+ actor_rollout_ref.actor.grad_clip=1.0 \
118
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
119
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
120
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
121
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
122
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
123
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
124
+ actor_rollout_ref.rollout.temperature=${temperature} \
125
+ actor_rollout_ref.rollout.top_p=${top_p} \
126
+ actor_rollout_ref.rollout.top_k=${top_k} \
127
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
128
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
129
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
130
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
131
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
132
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
133
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
134
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
135
+ actor_rollout_ref.rollout.name=${rollout_name} \
136
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
137
+ actor_rollout_ref.rollout.calculate_log_probs=True \
138
+ reward_model.reward_manager=dapo \
139
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
140
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
143
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
144
+ trainer.logger=['console','tensorboard'] \
145
+ trainer.project_name="${project_name}" \
146
+ trainer.experiment_name="${exp_name}" \
147
+ trainer.val_before_train=True \
148
+ trainer.save_freq=-1 \
149
+ trainer.default_local_dir="${CKPTS_DIR}" \
150
+ trainer.resume_mode=auto \
151
+ trainer.nnodes="${NNODES_TRAIN}" \
152
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
153
+ rollout.nnodes="${NNODES_ROLLOUT}" \
154
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
155
+ rollout.total_rollout_steps="${total_rollout_steps}" \
156
+ rollout.total_epochs=10 \
157
+ rollout.test_freq="${test_freq}" \
158
+ async_training.staleness_threshold="${staleness_threshold}" \
159
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
160
+ async_training.require_batches="${require_batches}" \
161
+ async_training.partial_rollout="${partial_rollout}" \
162
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64_mis.sh ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='dapo_qwen2-7B-math_28k_fsdp2_fully-async_64-64'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 28))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=4
60
+ sp_size=4
61
+ fsdp_size=8
62
+
63
+ # Fully async specific parameters
64
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-8}
65
+ NNODES_TRAIN=${NNODES_TRAIN:-8}
66
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
67
+
68
+ train_prompt_bsz=0
69
+ gen_prompt_bsz=1
70
+ n_resp_per_prompt=16
71
+ train_prompt_mini_bsz=32
72
+ total_rollout_steps=$(((512*400)))
73
+ test_freq=20
74
+ staleness_threshold=0.5
75
+ trigger_parameter_sync_step=4
76
+ require_batches=4
77
+ partial_rollout=True
78
+
79
+ # Rollout Correction
80
+ rollout_is=token
81
+ rollout_is_threshold=2.0
82
+ rollout_rs=seq_mean_k1
83
+ rollout_rs_threshold="0.99_1.001"
84
+
85
+ python -m verl.experimental.fully_async_policy.fully_async_main \
86
+ data.train_files="${TRAIN_FILE}" \
87
+ data.val_files="${TEST_FILE}" \
88
+ data.prompt_key=prompt \
89
+ data.truncation='left' \
90
+ data.max_prompt_length=${max_prompt_length} \
91
+ data.max_response_length=${max_response_length} \
92
+ data.train_batch_size=${train_prompt_bsz} \
93
+ data.gen_batch_size=${gen_prompt_bsz} \
94
+ data.return_raw_chat=${return_raw_chat} \
95
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
96
+ algorithm.adv_estimator=${adv_estimator} \
97
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
98
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
99
+ actor_rollout_ref.actor.strategy=fsdp2 \
100
+ critic.strategy=fsdp2 \
101
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
102
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
103
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
104
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
105
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
106
+ actor_rollout_ref.model.use_remove_padding=True \
107
+ actor_rollout_ref.hybrid_engine=False \
108
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
109
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
110
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
111
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
112
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
113
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
114
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
115
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
116
+ actor_rollout_ref.actor.optim.lr=1e-6 \
117
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
118
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
119
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
120
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
121
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
122
+ actor_rollout_ref.actor.entropy_coeff=0 \
123
+ actor_rollout_ref.actor.grad_clip=1.0 \
124
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
125
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
126
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
127
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
128
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
129
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
130
+ actor_rollout_ref.rollout.temperature=${temperature} \
131
+ actor_rollout_ref.rollout.top_p=${top_p} \
132
+ actor_rollout_ref.rollout.top_k=${top_k} \
133
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
134
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
135
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
136
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
137
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
138
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
139
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
140
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
141
+ actor_rollout_ref.rollout.name=${rollout_name} \
142
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
143
+ actor_rollout_ref.rollout.calculate_log_probs=True \
144
+ reward_model.reward_manager=dapo \
145
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
146
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
147
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
148
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
149
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
150
+ trainer.logger=['console','tensorboard'] \
151
+ trainer.project_name="${project_name}" \
152
+ trainer.experiment_name="${exp_name}" \
153
+ trainer.val_before_train=True \
154
+ trainer.save_freq=-1 \
155
+ trainer.default_local_dir="${CKPTS_DIR}" \
156
+ trainer.resume_mode=auto \
157
+ trainer.nnodes="${NNODES_TRAIN}" \
158
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
159
+ rollout.nnodes="${NNODES_ROLLOUT}" \
160
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
161
+ rollout.total_rollout_steps="${total_rollout_steps}" \
162
+ rollout.total_epochs=10 \
163
+ rollout.test_freq="${test_freq}" \
164
+ async_training.staleness_threshold="${staleness_threshold}" \
165
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
166
+ async_training.require_batches="${require_batches}" \
167
+ async_training.partial_rollout="${partial_rollout}" \
168
+ async_training.use_rollout_log_probs=True \
169
+ async_training.compute_prox_log_prob=True \
170
+ algorithm.rollout_correction.rollout_is=${rollout_is} \
171
+ algorithm.rollout_correction.rollout_is_threshold=${rollout_is_threshold} \
172
+ algorithm.rollout_correction.rollout_rs=${rollout_rs} \
173
+ algorithm.rollout_correction.rollout_rs_threshold=${rollout_rs_threshold}
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_8_8.sh ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='DAPO'
5
+ exp_name='DAPO-Qwen2.5-7b-MATH-0527a1-fsdp2-fully-async-8-8'
6
+
7
+ # Ray
8
+ # RAY_ADDRESS=${RAY_ADDRESS:-"http://localhost:8265"}
9
+ # WORKING_DIR=${WORKING_DIR:-"${PWD}"}
10
+ # RUNTIME_ENV=${RUNTIME_ENV:-"${WORKING_DIR}/verl/trainer/runtime_env.yaml"}
11
+ # Paths
12
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
13
+ # very important! please modify the max_position_embeddings in config.json to 32768 after downloading from huggingface
14
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-Math-7B"}
15
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
16
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
17
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
18
+
19
+ rollout_mode="async"
20
+ rollout_name="vllm" # sglang or vllm
21
+ if [ "$rollout_mode" = "async" ]; then
22
+ export VLLM_USE_V1=1
23
+ return_raw_chat="True"
24
+ fi
25
+
26
+ # Algorithm parameters
27
+ adv_estimator=grpo
28
+
29
+ use_kl_in_reward=False
30
+ kl_coef=0.0
31
+ use_kl_loss=False
32
+ kl_loss_coef=0.0
33
+
34
+ clip_ratio_low=0.2
35
+ clip_ratio_high=0.28
36
+
37
+ # Response length parameters
38
+ max_prompt_length=$((1024 * 2))
39
+ max_response_length=$((1024 * 8))
40
+ enable_overlong_buffer=True
41
+ overlong_buffer_len=$((1024 * 4))
42
+ overlong_penalty_factor=1.0
43
+
44
+ # Training parameters
45
+ loss_agg_mode="token-mean"
46
+
47
+ # Algorithm
48
+ temperature=1.0
49
+ top_p=1.0
50
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
51
+ val_top_p=0.7
52
+
53
+ # Performance Related Parameter
54
+ use_dynamic_bsz=True
55
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 2))
56
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length) * 3))
57
+ ref_offload=True
58
+ actor_offload=False
59
+ gen_tp=1
60
+ sp_size=1
61
+ fsdp_size=2
62
+
63
+ # Fully async specific parameters
64
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-1}
65
+ NNODES_TRAIN=${NNODES_TRAIN:-1}
66
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
67
+
68
+ train_prompt_bsz=0
69
+ gen_prompt_bsz=1
70
+ n_resp_per_prompt=16
71
+ train_prompt_mini_bsz=32
72
+ total_rollout_steps=$(((512*100)))
73
+ test_freq=10
74
+ staleness_threshold=0.1
75
+ trigger_parameter_sync_step=4
76
+ require_batches=4
77
+ partial_rollout=True
78
+
79
+ python -m verl.experimental.fully_async_policy.fully_async_main \
80
+ data.train_files="${TRAIN_FILE}" \
81
+ data.val_files="${TEST_FILE}" \
82
+ data.prompt_key=prompt \
83
+ data.truncation='left' \
84
+ data.max_prompt_length=${max_prompt_length} \
85
+ data.max_response_length=${max_response_length} \
86
+ data.train_batch_size=${train_prompt_bsz} \
87
+ data.gen_batch_size=${gen_prompt_bsz} \
88
+ data.return_raw_chat=${return_raw_chat} \
89
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
90
+ algorithm.adv_estimator=${adv_estimator} \
91
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
92
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
93
+ actor_rollout_ref.actor.strategy=fsdp2 \
94
+ critic.strategy=fsdp2 \
95
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
96
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
97
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
98
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
99
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
100
+ actor_rollout_ref.model.use_remove_padding=True \
101
+ actor_rollout_ref.hybrid_engine=False \
102
+ +actor_rollout_ref.model.override_config.max_position_embeddings=32768 \
103
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
104
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
105
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=${use_dynamic_bsz} \
106
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
107
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
108
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
109
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
110
+ actor_rollout_ref.actor.optim.lr=1e-6 \
111
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
112
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
113
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
114
+ actor_rollout_ref.actor.fsdp_config.param_offload=${actor_offload} \
115
+ actor_rollout_ref.actor.fsdp_config.optimizer_offload=${actor_offload} \
116
+ actor_rollout_ref.actor.entropy_coeff=0 \
117
+ actor_rollout_ref.actor.grad_clip=1.0 \
118
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
119
+ actor_rollout_ref.actor.ulysses_sequence_parallel_size=${sp_size} \
120
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.80 \
121
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${gen_tp} \
122
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
123
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
124
+ actor_rollout_ref.rollout.temperature=${temperature} \
125
+ actor_rollout_ref.rollout.top_p=${top_p} \
126
+ actor_rollout_ref.rollout.top_k=${top_k} \
127
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
128
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
129
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
130
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
131
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
132
+ actor_rollout_ref.ref.fsdp_config.param_offload=${ref_offload} \
133
+ actor_rollout_ref.ref.ulysses_sequence_parallel_size=${sp_size} \
134
+ actor_rollout_ref.actor.fsdp_config.fsdp_size=${fsdp_size} \
135
+ actor_rollout_ref.rollout.name=${rollout_name} \
136
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
137
+ actor_rollout_ref.rollout.calculate_log_probs=True \
138
+ reward_model.reward_manager=dapo \
139
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
140
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
141
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
142
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
143
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
144
+ trainer.logger=['console','tensorboard'] \
145
+ trainer.project_name="${project_name}" \
146
+ trainer.experiment_name="${exp_name}" \
147
+ trainer.val_before_train=True \
148
+ trainer.save_freq=-1 \
149
+ trainer.default_local_dir="${CKPTS_DIR}" \
150
+ trainer.resume_mode=auto \
151
+ trainer.nnodes="${NNODES_TRAIN}" \
152
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
153
+ rollout.nnodes="${NNODES_ROLLOUT}" \
154
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
155
+ rollout.total_rollout_steps="${total_rollout_steps}" \
156
+ rollout.total_epochs=10 \
157
+ rollout.test_freq="${test_freq}" \
158
+ async_training.staleness_threshold="${staleness_threshold}" \
159
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
160
+ async_training.require_batches="${require_batches}" \
161
+ async_training.partial_rollout="${partial_rollout}" \
162
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/geo3k_qwen25vl_7b_megatron_4_4.sh ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ set -x
2
+ ENGINE=${1:-vllm}
3
+ export CUDA_DEVICE_MAX_CONNECTIONS=1 # For megatron communication/computation overlapping
4
+
5
+
6
+ HF_MODEL_PATH=${HF_MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen2.5-VL-7B-Instruct"}
7
+
8
+ train_path=$HOME/data/geo3k/train.parquet
9
+ test_path=$HOME/data/geo3k/test.parquet
10
+
11
+ rollout_mode="async"
12
+ rollout_name="vllm" # sglang or vllm
13
+ if [ "$rollout_mode" = "async" ]; then
14
+ export VLLM_USE_V1=1
15
+ return_raw_chat="True"
16
+ fi
17
+
18
+ # Fully async specific parameters
19
+ NNODES=${NNODES:-1}
20
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
21
+
22
+ n_gpus_rollout=4
23
+ n_gpus_training=$((NGPUS_PER_NODE - n_gpus_rollout))
24
+
25
+ train_prompt_bsz=0
26
+ gen_prompt_bsz=1
27
+ n_resp_per_prompt=4
28
+ train_prompt_mini_bsz=128
29
+ total_rollout_steps=$(((512*100)))
30
+ test_freq=5
31
+ staleness_threshold=0.1
32
+ trigger_parameter_sync_step=4
33
+ require_batches=2
34
+ partial_rollout=True
35
+ total_epochs=200
36
+
37
+ python -m verl.experimental.fully_async_policy.fully_async_main \
38
+ --config-path=config \
39
+ --config-name='fully_async_ppo_megatron_trainer.yaml'\
40
+ algorithm.adv_estimator=grpo \
41
+ data.train_files="$train_path" \
42
+ data.val_files="$test_path" \
43
+ data.train_batch_size=${train_prompt_bsz} \
44
+ data.max_prompt_length=1024 \
45
+ data.max_response_length=2048 \
46
+ actor_rollout_ref.rollout.max_model_len=32768 \
47
+ actor_rollout_ref.rollout.max_num_batched_tokens=32768 \
48
+ data.filter_overlong_prompts=True \
49
+ data.truncation='error' \
50
+ data.gen_batch_size=${gen_prompt_bsz} \
51
+ data.return_raw_chat=${return_raw_chat} \
52
+ actor_rollout_ref.model.path=$HF_MODEL_PATH \
53
+ actor_rollout_ref.actor.optim.lr=1e-6 \
54
+ actor_rollout_ref.actor.optim.lr_decay_steps=51200 \
55
+ actor_rollout_ref.hybrid_engine=False \
56
+ actor_rollout_ref.rollout.calculate_log_probs=True \
57
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
58
+ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=1 \
59
+ actor_rollout_ref.actor.megatron.pipeline_model_parallel_size=1 \
60
+ actor_rollout_ref.actor.megatron.tensor_model_parallel_size=2 \
61
+ actor_rollout_ref.actor.use_kl_loss=True \
62
+ actor_rollout_ref.actor.kl_loss_coef=0.01 \
63
+ actor_rollout_ref.actor.kl_loss_type=low_var_kl \
64
+ actor_rollout_ref.actor.entropy_coeff=0 \
65
+ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=1 \
66
+ actor_rollout_ref.rollout.tensor_model_parallel_size=1 \
67
+ actor_rollout_ref.actor.use_dynamic_bsz=True \
68
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=5120 \
69
+ actor_rollout_ref.ref.log_prob_use_dynamic_bsz=True \
70
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=5120 \
71
+ actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=True \
72
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=5120 \
73
+ actor_rollout_ref.rollout.name=$ENGINE \
74
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
75
+ +actor_rollout_ref.rollout.engine_kwargs.vllm.disable_mm_preprocessor_cache=True \
76
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.8 \
77
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
78
+ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=1 \
79
+ actor_rollout_ref.ref.megatron.pipeline_model_parallel_size=1 \
80
+ actor_rollout_ref.ref.megatron.tensor_model_parallel_size=4 \
81
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_offload_fraction=1 \
82
+ +actor_rollout_ref.actor.optim.override_optimizer_config.overlap_cpu_optimizer_d2h_h2d=True \
83
+ +actor_rollout_ref.actor.optim.override_optimizer_config.use_precision_aware_optimizer=True \
84
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_cpu_offload=True \
85
+ actor_rollout_ref.actor.megatron.use_mbridge=True \
86
+ actor_rollout_ref.actor.megatron.param_offload=True \
87
+ actor_rollout_ref.actor.megatron.optimizer_offload=True \
88
+ actor_rollout_ref.actor.megatron.grad_offload=True \
89
+ actor_rollout_ref.ref.megatron.param_offload=True \
90
+ algorithm.use_kl_in_reward=False \
91
+ trainer.critic_warmup=0 \
92
+ trainer.logger='["console","wandb"]' \
93
+ trainer.project_name='verl_grpo_example_geo3k' \
94
+ trainer.experiment_name='qwen2_5_vl_7b_megatron_async' \
95
+ trainer.test_freq="${test_freq}" \
96
+ trainer.total_epochs="${total_epochs}" \
97
+ trainer.val_before_train=False \
98
+ trainer.save_freq=-1 \
99
+ trainer.resume_mode=auto \
100
+ trainer.nnodes="${NNODES}" \
101
+ trainer.n_gpus_per_node="${n_gpus_training}" \
102
+ rollout.nnodes="${NNODES}" \
103
+ rollout.n_gpus_per_node="${n_gpus_rollout}" \
104
+ rollout.total_rollout_steps="${total_rollout_steps}" \
105
+ rollout.total_epochs="${total_epochs}" \
106
+ rollout.test_freq="${test_freq}" \
107
+ async_training.staleness_threshold="${staleness_threshold}" \
108
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
109
+ async_training.require_batches="${require_batches}" \
110
+ async_training.partial_rollout="${partial_rollout}" \
111
+ async_training.use_rollout_log_probs=True
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32.sh ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='GRPO-Qwen3-30b-Base-MATH'
5
+ exp_name='GRPO-Qwen3-30b-Base-MATH-megatron-fully-async_96-32'
6
+
7
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
8
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen3-30B-A3B-Base"}
9
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
10
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
11
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
12
+
13
+ rollout_mode="async"
14
+ rollout_name="vllm" # sglang or vllm
15
+ if [ "$rollout_mode" = "async" ]; then
16
+ export VLLM_USE_V1=1
17
+ return_raw_chat="True"
18
+ fi
19
+ # Algorithm parameters
20
+ adv_estimator=grpo
21
+
22
+ use_kl_in_reward=False
23
+ kl_coef=0.0
24
+ use_kl_loss=True
25
+ kl_loss_coef=0.001
26
+ kl_loss_type=low_var_kl
27
+
28
+ clip_ratio_low=0.2
29
+ clip_ratio_high=0.28
30
+
31
+ # Response length parameters
32
+ max_prompt_length=$((1024 * 2))
33
+ max_response_length=$((1024 * 8))
34
+ enable_overlong_buffer=True
35
+ overlong_buffer_len=$((1024 * 4))
36
+ overlong_penalty_factor=1.0
37
+
38
+ loss_agg_mode="token-mean"
39
+
40
+ # Algorithm
41
+ temperature=1.0
42
+ top_p=1.0
43
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
44
+ val_top_p=0.7
45
+
46
+ # Performance Related Parameter
47
+ use_dynamic_bsz=True
48
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length)))
49
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length)))
50
+ offload=True
51
+ train_ppo_micro_batch_size_per_gpu=2
52
+ infer_ppo_micro_batch_size_per_gpu=2
53
+
54
+ optimizer_offload_fraction=${OFFLOAD_FRACTION:-1.}
55
+
56
+ COMMON_PP=${COMMON_PP:-1}
57
+ COMMON_VPP=${COMMON_VPP:-null}
58
+ COMMON_CP=${COMMON_CP:-2}
59
+ COMMON_TP=${COMMON_TP:-2}
60
+ COMMON_EP=${COMMON_EP:-8}
61
+ COMMON_ETP=${COMMON_ETP:-1}
62
+
63
+ TRAIN_TP=${TRAIN_TP:-$COMMON_TP}
64
+ INFER_TP=${INFER_TP:-4}
65
+
66
+ ACTOR_PP=${ACTOR_PP:-$COMMON_PP}
67
+ ACTOR_VPP=${ACTOR_VPP:-$COMMON_VPP}
68
+ ACTOR_CP=${ACTOR_CP:-$COMMON_CP}
69
+ ACTOR_TP=${ACTOR_TP:-$TRAIN_TP}
70
+ ACTOR_EP=${ACTOR_EP:-$COMMON_EP}
71
+ ACTOR_ETP=${ACTOR_ETP:-$COMMON_ETP}
72
+ ROLLOUT_TP=${ROLLOUT_TP:-$INFER_TP}
73
+ REF_PP=${REF_PP:-$COMMON_PP}
74
+ REF_VPP=${REF_VPP:-$COMMON_VPP}
75
+ REF_CP=${REF_CP:-$COMMON_CP}
76
+ REF_TP=${REF_TP:-$TRAIN_TP}
77
+ REF_EP=${REF_EP:-$COMMON_EP}
78
+ REF_ETP=${REF_ETP:-$COMMON_ETP}
79
+ CRITIC_PP=${CRITIC_PP:-$COMMON_PP}
80
+ CRITIC_VPP=${CRITIC_VPP:-$COMMON_VPP}
81
+ CRITIC_CP=${CRITIC_CP:-$COMMON_CP}
82
+ CRITIC_TP=${CRITIC_TP:-$TRAIN_TP}
83
+ CRITIC_EP=${CRITIC_EP:-$COMMON_EP}
84
+ CRITIC_ETP=${CRITIC_ETP:-$COMMON_ETP}
85
+ RM_PP=${RM_PP:-$COMMON_PP}
86
+ RM_VPP=${RM_VPP:-$COMMON_VPP}
87
+ RM_CP=${RM_CP:-$COMMON_CP}
88
+ RM_TP=${RM_TP:-$TRAIN_TP}
89
+ RM_EP=${RM_EP:-$COMMON_EP}
90
+ RM_ETP=${RM_ETP:-$COMMON_ETP}
91
+
92
+ # install mbridge
93
+ # pip3 install git+https://github.com/ISEEKYAN/mbridge
94
+ USE_MBRIDGE=True
95
+ USE_DIST_CKPT=False
96
+
97
+ # Fully async specific parameters
98
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-12}
99
+ NNODES_TRAIN=${NNODES_TRAIN:-4}
100
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
101
+
102
+ train_prompt_bsz=0
103
+ gen_prompt_bsz=1
104
+ n_resp_per_prompt=16
105
+ train_prompt_mini_bsz=128
106
+ total_rollout_steps=$(((512*400)))
107
+ test_freq=20
108
+ staleness_threshold=0.5
109
+ trigger_parameter_sync_step=4
110
+ require_batches=1
111
+ partial_rollout=True
112
+
113
+ python -m verl.experimental.fully_async_policy.fully_async_main \
114
+ --config-path=config \
115
+ --config-name='fully_async_ppo_megatron_trainer.yaml'\
116
+ data.train_files="${TRAIN_FILE}" \
117
+ data.val_files="${TEST_FILE}" \
118
+ data.prompt_key=prompt \
119
+ data.truncation='left' \
120
+ data.max_prompt_length=${max_prompt_length} \
121
+ data.max_response_length=${max_response_length} \
122
+ data.train_batch_size=${train_prompt_bsz} \
123
+ data.return_raw_chat=${return_raw_chat} \
124
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
125
+ algorithm.adv_estimator=${adv_estimator} \
126
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
127
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
128
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
129
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
130
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
131
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
132
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
133
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
134
+ +actor_rollout_ref.model.override_config.model_config.max_position_embeddings=$((max_prompt_length + max_response_length)) \
135
+ actor_rollout_ref.model.use_fused_kernels=False \
136
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
137
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
138
+ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=${train_ppo_micro_batch_size_per_gpu} \
139
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
140
+ actor_rollout_ref.actor.optim.lr=1e-6 \
141
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
142
+ actor_rollout_ref.actor.optim.lr_decay_style='constant' \
143
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
144
+ actor_rollout_ref.actor.optim.lr_decay_steps=${total_rollout_steps} \
145
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_offload_fraction=${optimizer_offload_fraction} \
146
+ +actor_rollout_ref.actor.optim.override_optimizer_config.overlap_cpu_optimizer_d2h_h2d=True \
147
+ +actor_rollout_ref.actor.optim.override_optimizer_config.use_precision_aware_optimizer=True \
148
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_cpu_offload=True \
149
+ actor_rollout_ref.actor.megatron.use_mbridge=$USE_MBRIDGE \
150
+ actor_rollout_ref.actor.megatron.use_dist_checkpointing=$USE_DIST_CKPT \
151
+ actor_rollout_ref.actor.megatron.param_offload=${offload} \
152
+ actor_rollout_ref.actor.megatron.grad_offload=${offload} \
153
+ actor_rollout_ref.actor.megatron.optimizer_offload=${offload} \
154
+ actor_rollout_ref.actor.megatron.tensor_model_parallel_size=${ACTOR_TP} \
155
+ actor_rollout_ref.actor.megatron.pipeline_model_parallel_size=${ACTOR_PP} \
156
+ actor_rollout_ref.actor.megatron.virtual_pipeline_model_parallel_size=${ACTOR_VPP} \
157
+ actor_rollout_ref.actor.megatron.context_parallel_size=${ACTOR_CP} \
158
+ actor_rollout_ref.actor.megatron.expert_model_parallel_size=${ACTOR_EP} \
159
+ actor_rollout_ref.actor.megatron.expert_tensor_parallel_size=${ACTOR_ETP} \
160
+ +actor_rollout_ref.actor.megatron.override_transformer_config.apply_rope_fusion=True \
161
+ +actor_rollout_ref.actor.megatron.override_transformer_config.masked_softmax_fusion=True \
162
+ +actor_rollout_ref.actor.megatron.override_transformer_config.bias_activation_fusion=True \
163
+ +actor_rollout_ref.actor.megatron.override_transformer_config.bias_dropout_fusion=True \
164
+ +actor_rollout_ref.actor.megatron.override_transformer_config.gradient_accumulation_fusion=True \
165
+ +actor_rollout_ref.actor.megatron.override_transformer_config.deallocate_pipeline_outputs=True \
166
+ +actor_rollout_ref.actor.megatron.override_transformer_config.persist_layer_norm=True \
167
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_grouped_gemm=True \
168
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_permute_fusion=True \
169
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_token_dispatcher_type="flex" \
170
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_router_dtype=fp32 \
171
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_enable_deepep=True \
172
+ actor_rollout_ref.actor.entropy_coeff=0 \
173
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
174
+ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=${infer_ppo_micro_batch_size_per_gpu} \
175
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
176
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.7 \
177
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${INFER_TP} \
178
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
179
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
180
+ actor_rollout_ref.rollout.temperature=${temperature} \
181
+ actor_rollout_ref.rollout.top_p=${top_p} \
182
+ actor_rollout_ref.rollout.top_k=${top_k} \
183
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
184
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
185
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
186
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
187
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
188
+ actor_rollout_ref.rollout.name=${rollout_name} \
189
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
190
+ actor_rollout_ref.rollout.calculate_log_probs=True \
191
+ actor_rollout_ref.hybrid_engine=False \
192
+ actor_rollout_ref.rollout.enforce_eager=True \
193
+ actor_rollout_ref.rollout.free_cache_engine=True \
194
+ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=${infer_ppo_micro_batch_size_per_gpu} \
195
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
196
+ actor_rollout_ref.ref.megatron.use_dist_checkpointing=${USE_DIST_CKPT} \
197
+ actor_rollout_ref.ref.megatron.param_offload=${offload} \
198
+ actor_rollout_ref.ref.megatron.tensor_model_parallel_size=${REF_TP} \
199
+ actor_rollout_ref.ref.megatron.pipeline_model_parallel_size=${REF_PP} \
200
+ actor_rollout_ref.ref.megatron.virtual_pipeline_model_parallel_size=${REF_VPP} \
201
+ actor_rollout_ref.ref.megatron.context_parallel_size=${REF_CP} \
202
+ actor_rollout_ref.ref.megatron.expert_model_parallel_size=${REF_EP} \
203
+ actor_rollout_ref.ref.megatron.expert_tensor_parallel_size=${REF_ETP} \
204
+ reward_model.reward_manager=dapo \
205
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
206
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
207
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
208
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
209
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
210
+ trainer.logger=['console','tensorboard'] \
211
+ trainer.project_name="${project_name}" \
212
+ trainer.experiment_name="${exp_name}" \
213
+ trainer.val_before_train=True \
214
+ trainer.save_freq=-1 \
215
+ trainer.total_epochs=10 \
216
+ trainer.resume_mode=auto \
217
+ trainer.log_val_generations=10 \
218
+ trainer.nnodes="${NNODES_TRAIN}" \
219
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
220
+ rollout.nnodes="${NNODES_ROLLOUT}" \
221
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
222
+ rollout.total_rollout_steps="${total_rollout_steps}" \
223
+ rollout.total_epochs=10 \
224
+ rollout.test_freq="${test_freq}" \
225
+ async_training.staleness_threshold="${staleness_threshold}" \
226
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
227
+ async_training.require_batches="${require_batches}" \
228
+ async_training.partial_rollout="${partial_rollout}" \
229
+ async_training.use_rollout_log_probs=True \
230
+
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32_mis.sh ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -xeuo pipefail
3
+
4
+ project_name='GRPO-Qwen3-30b-Base-MATH'
5
+ exp_name='GRPO-Qwen3-30b-Base-MATH-megatron-fully-async_96-32'
6
+
7
+ RAY_DATA_HOME=${RAY_DATA_HOME:-"${HOME}/verl"}
8
+ MODEL_PATH=${MODEL_PATH:-"${RAY_DATA_HOME}/models/Qwen3-30B-A3B-Base"}
9
+ CKPTS_DIR=${CKPTS_DIR:-"${RAY_DATA_HOME}/ckpts/${project_name}/${exp_name}"}
10
+ TRAIN_FILE=${TRAIN_FILE:-"${RAY_DATA_HOME}/data/dapo-math-17k.parquet"}
11
+ TEST_FILE=${TEST_FILE:-"${RAY_DATA_HOME}/data/aime-2024.parquet"}
12
+
13
+ rollout_mode="async"
14
+ rollout_name="vllm" # sglang or vllm
15
+ if [ "$rollout_mode" = "async" ]; then
16
+ export VLLM_USE_V1=1
17
+ return_raw_chat="True"
18
+ fi
19
+ # Algorithm parameters
20
+ adv_estimator=grpo
21
+
22
+ use_kl_in_reward=False
23
+ kl_coef=0.0
24
+ use_kl_loss=True
25
+ kl_loss_coef=0.001
26
+ kl_loss_type=low_var_kl
27
+
28
+ clip_ratio_low=0.2
29
+ clip_ratio_high=0.28
30
+
31
+ # Response length parameters
32
+ max_prompt_length=$((1024 * 2))
33
+ max_response_length=$((1024 * 8))
34
+ enable_overlong_buffer=True
35
+ overlong_buffer_len=$((1024 * 4))
36
+ overlong_penalty_factor=1.0
37
+
38
+ loss_agg_mode="token-mean"
39
+
40
+ # Algorithm
41
+ temperature=1.0
42
+ top_p=1.0
43
+ top_k=-1 # 0 for HF rollout, -1 for vLLM rollout
44
+ val_top_p=0.7
45
+
46
+ # Performance Related Parameter
47
+ use_dynamic_bsz=True
48
+ actor_ppo_max_token_len=$(((max_prompt_length + max_response_length)))
49
+ infer_ppo_max_token_len=$(((max_prompt_length + max_response_length)))
50
+ offload=True
51
+ train_ppo_micro_batch_size_per_gpu=2
52
+ infer_ppo_micro_batch_size_per_gpu=2
53
+
54
+ optimizer_offload_fraction=${OFFLOAD_FRACTION:-1.}
55
+
56
+ COMMON_PP=${COMMON_PP:-1}
57
+ COMMON_VPP=${COMMON_VPP:-null}
58
+ COMMON_CP=${COMMON_CP:-2}
59
+ COMMON_TP=${COMMON_TP:-2}
60
+ COMMON_EP=${COMMON_EP:-8}
61
+ COMMON_ETP=${COMMON_ETP:-1}
62
+
63
+ TRAIN_TP=${TRAIN_TP:-$COMMON_TP}
64
+ INFER_TP=${INFER_TP:-4}
65
+
66
+ ACTOR_PP=${ACTOR_PP:-$COMMON_PP}
67
+ ACTOR_VPP=${ACTOR_VPP:-$COMMON_VPP}
68
+ ACTOR_CP=${ACTOR_CP:-$COMMON_CP}
69
+ ACTOR_TP=${ACTOR_TP:-$TRAIN_TP}
70
+ ACTOR_EP=${ACTOR_EP:-$COMMON_EP}
71
+ ACTOR_ETP=${ACTOR_ETP:-$COMMON_ETP}
72
+ ROLLOUT_TP=${ROLLOUT_TP:-$INFER_TP}
73
+ REF_PP=${REF_PP:-$COMMON_PP}
74
+ REF_VPP=${REF_VPP:-$COMMON_VPP}
75
+ REF_CP=${REF_CP:-$COMMON_CP}
76
+ REF_TP=${REF_TP:-$TRAIN_TP}
77
+ REF_EP=${REF_EP:-$COMMON_EP}
78
+ REF_ETP=${REF_ETP:-$COMMON_ETP}
79
+ CRITIC_PP=${CRITIC_PP:-$COMMON_PP}
80
+ CRITIC_VPP=${CRITIC_VPP:-$COMMON_VPP}
81
+ CRITIC_CP=${CRITIC_CP:-$COMMON_CP}
82
+ CRITIC_TP=${CRITIC_TP:-$TRAIN_TP}
83
+ CRITIC_EP=${CRITIC_EP:-$COMMON_EP}
84
+ CRITIC_ETP=${CRITIC_ETP:-$COMMON_ETP}
85
+ RM_PP=${RM_PP:-$COMMON_PP}
86
+ RM_VPP=${RM_VPP:-$COMMON_VPP}
87
+ RM_CP=${RM_CP:-$COMMON_CP}
88
+ RM_TP=${RM_TP:-$TRAIN_TP}
89
+ RM_EP=${RM_EP:-$COMMON_EP}
90
+ RM_ETP=${RM_ETP:-$COMMON_ETP}
91
+
92
+ # install mbridge
93
+ # pip3 install git+https://github.com/ISEEKYAN/mbridge
94
+ USE_MBRIDGE=True
95
+ USE_DIST_CKPT=False
96
+
97
+ # Fully async specific parameters
98
+ NNODES_ROLLOUT=${NNODES_ROLLOUT:-12}
99
+ NNODES_TRAIN=${NNODES_TRAIN:-4}
100
+ NGPUS_PER_NODE=${NGPUS_PER_NODE:-8}
101
+
102
+ train_prompt_bsz=0
103
+ gen_prompt_bsz=1
104
+ n_resp_per_prompt=16
105
+ train_prompt_mini_bsz=128
106
+ total_rollout_steps=$(((512*400)))
107
+ test_freq=20
108
+ staleness_threshold=0.5
109
+ trigger_parameter_sync_step=4
110
+ require_batches=1
111
+ partial_rollout=True
112
+
113
+ # Rollout Importance Sampling
114
+
115
+ rollout_is=null
116
+ rollout_rs=seq_mean_k1
117
+ rollout_rs_threshold="0.999_1.001"
118
+
119
+ python -m verl.experimental.fully_async_policy.fully_async_main \
120
+ --config-path=config \
121
+ --config-name='fully_async_ppo_megatron_trainer.yaml'\
122
+ data.train_files="${TRAIN_FILE}" \
123
+ data.val_files="${TEST_FILE}" \
124
+ data.prompt_key=prompt \
125
+ data.truncation='left' \
126
+ data.max_prompt_length=${max_prompt_length} \
127
+ data.max_response_length=${max_response_length} \
128
+ data.train_batch_size=${train_prompt_bsz} \
129
+ data.return_raw_chat=${return_raw_chat} \
130
+ actor_rollout_ref.rollout.n=${n_resp_per_prompt} \
131
+ algorithm.adv_estimator=${adv_estimator} \
132
+ algorithm.use_kl_in_reward=${use_kl_in_reward} \
133
+ algorithm.kl_ctrl.kl_coef=${kl_coef} \
134
+ async_training.compute_prox_log_prob=True \
135
+ algorithm.rollout_correction.rollout_is=${rollout_is} \
136
+ algorithm.rollout_correction.rollout_rs=${rollout_rs} \
137
+ algorithm.rollout_correction.rollout_rs_threshold=${rollout_rs_threshold} \
138
+ actor_rollout_ref.model.path="${MODEL_PATH}" \
139
+ actor_rollout_ref.actor.use_kl_loss=${use_kl_loss} \
140
+ actor_rollout_ref.actor.kl_loss_coef=${kl_loss_coef} \
141
+ actor_rollout_ref.actor.clip_ratio_low=${clip_ratio_low} \
142
+ actor_rollout_ref.actor.clip_ratio_high=${clip_ratio_high} \
143
+ actor_rollout_ref.actor.clip_ratio_c=10.0 \
144
+ +actor_rollout_ref.model.override_config.model_config.max_position_embeddings=$((max_prompt_length + max_response_length)) \
145
+ actor_rollout_ref.model.use_fused_kernels=False \
146
+ actor_rollout_ref.actor.use_dynamic_bsz=${use_dynamic_bsz} \
147
+ actor_rollout_ref.actor.ppo_mini_batch_size=${train_prompt_mini_bsz} \
148
+ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=${train_ppo_micro_batch_size_per_gpu} \
149
+ actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${actor_ppo_max_token_len} \
150
+ actor_rollout_ref.actor.optim.lr=1e-6 \
151
+ actor_rollout_ref.actor.optim.lr_warmup_steps=10 \
152
+ actor_rollout_ref.actor.optim.lr_decay_style='constant' \
153
+ actor_rollout_ref.actor.optim.weight_decay=0.1 \
154
+ actor_rollout_ref.actor.optim.lr_decay_steps=${total_rollout_steps} \
155
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_offload_fraction=${optimizer_offload_fraction} \
156
+ +actor_rollout_ref.actor.optim.override_optimizer_config.overlap_cpu_optimizer_d2h_h2d=True \
157
+ +actor_rollout_ref.actor.optim.override_optimizer_config.use_precision_aware_optimizer=True \
158
+ +actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_cpu_offload=True \
159
+ actor_rollout_ref.actor.megatron.use_mbridge=$USE_MBRIDGE \
160
+ actor_rollout_ref.actor.megatron.use_dist_checkpointing=$USE_DIST_CKPT \
161
+ actor_rollout_ref.actor.megatron.param_offload=${offload} \
162
+ actor_rollout_ref.actor.megatron.grad_offload=${offload} \
163
+ actor_rollout_ref.actor.megatron.optimizer_offload=${offload} \
164
+ actor_rollout_ref.actor.megatron.tensor_model_parallel_size=${ACTOR_TP} \
165
+ actor_rollout_ref.actor.megatron.pipeline_model_parallel_size=${ACTOR_PP} \
166
+ actor_rollout_ref.actor.megatron.virtual_pipeline_model_parallel_size=${ACTOR_VPP} \
167
+ actor_rollout_ref.actor.megatron.context_parallel_size=${ACTOR_CP} \
168
+ actor_rollout_ref.actor.megatron.expert_model_parallel_size=${ACTOR_EP} \
169
+ actor_rollout_ref.actor.megatron.expert_tensor_parallel_size=${ACTOR_ETP} \
170
+ +actor_rollout_ref.actor.megatron.override_transformer_config.apply_rope_fusion=True \
171
+ +actor_rollout_ref.actor.megatron.override_transformer_config.masked_softmax_fusion=True \
172
+ +actor_rollout_ref.actor.megatron.override_transformer_config.bias_activation_fusion=True \
173
+ +actor_rollout_ref.actor.megatron.override_transformer_config.bias_dropout_fusion=True \
174
+ +actor_rollout_ref.actor.megatron.override_transformer_config.gradient_accumulation_fusion=True \
175
+ +actor_rollout_ref.actor.megatron.override_transformer_config.deallocate_pipeline_outputs=True \
176
+ +actor_rollout_ref.actor.megatron.override_transformer_config.persist_layer_norm=True \
177
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_grouped_gemm=True \
178
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_permute_fusion=True \
179
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_token_dispatcher_type="flex" \
180
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_router_dtype=fp32 \
181
+ +actor_rollout_ref.actor.megatron.override_transformer_config.moe_enable_deepep=True \
182
+ actor_rollout_ref.actor.entropy_coeff=0 \
183
+ actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode} \
184
+ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=${infer_ppo_micro_batch_size_per_gpu} \
185
+ actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
186
+ actor_rollout_ref.rollout.gpu_memory_utilization=0.7 \
187
+ actor_rollout_ref.rollout.tensor_model_parallel_size=${INFER_TP} \
188
+ actor_rollout_ref.rollout.enable_chunked_prefill=True \
189
+ actor_rollout_ref.rollout.max_num_batched_tokens=$((max_prompt_length + max_response_length)) \
190
+ actor_rollout_ref.rollout.temperature=${temperature} \
191
+ actor_rollout_ref.rollout.top_p=${top_p} \
192
+ actor_rollout_ref.rollout.top_k=${top_k} \
193
+ actor_rollout_ref.rollout.val_kwargs.temperature=${temperature} \
194
+ actor_rollout_ref.rollout.val_kwargs.top_p=${val_top_p} \
195
+ actor_rollout_ref.rollout.val_kwargs.top_k=${top_k} \
196
+ actor_rollout_ref.rollout.val_kwargs.do_sample=True \
197
+ actor_rollout_ref.rollout.val_kwargs.n=1 \
198
+ actor_rollout_ref.rollout.name=${rollout_name} \
199
+ actor_rollout_ref.rollout.mode=${rollout_mode} \
200
+ actor_rollout_ref.rollout.calculate_log_probs=True \
201
+ actor_rollout_ref.hybrid_engine=False \
202
+ actor_rollout_ref.rollout.enforce_eager=True \
203
+ actor_rollout_ref.rollout.free_cache_engine=True \
204
+ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=${infer_ppo_micro_batch_size_per_gpu} \
205
+ actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_ppo_max_token_len} \
206
+ actor_rollout_ref.ref.megatron.use_dist_checkpointing=${USE_DIST_CKPT} \
207
+ actor_rollout_ref.ref.megatron.param_offload=${offload} \
208
+ actor_rollout_ref.ref.megatron.tensor_model_parallel_size=${REF_TP} \
209
+ actor_rollout_ref.ref.megatron.pipeline_model_parallel_size=${REF_PP} \
210
+ actor_rollout_ref.ref.megatron.virtual_pipeline_model_parallel_size=${REF_VPP} \
211
+ actor_rollout_ref.ref.megatron.context_parallel_size=${REF_CP} \
212
+ actor_rollout_ref.ref.megatron.expert_model_parallel_size=${REF_EP} \
213
+ actor_rollout_ref.ref.megatron.expert_tensor_parallel_size=${REF_ETP} \
214
+ reward_model.reward_manager=dapo \
215
+ +reward_model.reward_kwargs.overlong_buffer_cfg.enable=${enable_overlong_buffer} \
216
+ +reward_model.reward_kwargs.overlong_buffer_cfg.len=${overlong_buffer_len} \
217
+ +reward_model.reward_kwargs.overlong_buffer_cfg.penalty_factor=${overlong_penalty_factor} \
218
+ +reward_model.reward_kwargs.overlong_buffer_cfg.log=False \
219
+ +reward_model.reward_kwargs.max_resp_len=${max_response_length} \
220
+ trainer.logger=['console','tensorboard'] \
221
+ trainer.project_name="${project_name}" \
222
+ trainer.experiment_name="${exp_name}" \
223
+ trainer.val_before_train=True \
224
+ trainer.save_freq=-1 \
225
+ trainer.total_epochs=10 \
226
+ trainer.resume_mode=auto \
227
+ trainer.log_val_generations=10 \
228
+ trainer.nnodes="${NNODES_TRAIN}" \
229
+ trainer.n_gpus_per_node="${NGPUS_PER_NODE}" \
230
+ rollout.nnodes="${NNODES_ROLLOUT}" \
231
+ rollout.n_gpus_per_node="${NGPUS_PER_NODE}" \
232
+ rollout.total_rollout_steps="${total_rollout_steps}" \
233
+ rollout.total_epochs=10 \
234
+ rollout.test_freq="${test_freq}" \
235
+ async_training.staleness_threshold="${staleness_threshold}" \
236
+ async_training.trigger_parameter_sync_step="${trigger_parameter_sync_step}" \
237
+ async_training.require_batches="${require_batches}" \
238
+ async_training.partial_rollout="${partial_rollout}" \
239
+ async_training.use_rollout_log_probs=True \
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/runtime_env.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ env_vars:
2
+ VLLM_USE_V1: "1"
3
+ NCCL_DEBUG: "INFO"
4
+ HYDRA_FULL_ERROR: "1"
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/unittest/simple_streaming_demo.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import asyncio
16
+ import random
17
+ import time
18
+
19
+
20
+ class SimpleStreamingSystem:
21
+ """Simplified streaming system demonstration"""
22
+
23
+ def __init__(self, max_concurrent_tasks: int = 4):
24
+ self.max_concurrent_tasks = max_concurrent_tasks
25
+ self.data_queue = asyncio.Queue()
26
+ self.result_queue = asyncio.Queue()
27
+ self.consumer_count = 0
28
+
29
+ # Data stream coroutine
30
+ async def data_stream(self):
31
+ # Add initial data
32
+ # Prepare test data
33
+ test_data = [{"id": f"task_{i}", "content": f"data_{i}"} for i in range(8)]
34
+ await self.add_data_stream(test_data)
35
+
36
+ # Simulate subsequent data stream
37
+ await asyncio.sleep(3)
38
+ print("\nAdding second batch of data...")
39
+ extra_data = [{"id": f"extra_{i}", "content": f"extra_data_{i}"} for i in range(5)]
40
+ await self.add_data_stream(extra_data)
41
+
42
+ # Send termination signal
43
+ await asyncio.sleep(1)
44
+ await self.data_queue.put("DONE")
45
+ print("Sending termination signal")
46
+
47
+ async def add_data_stream(self, data_list: list[dict]):
48
+ """Simulate data stream"""
49
+ print("Starting to add data stream...")
50
+
51
+ for i, data_item in enumerate(data_list):
52
+ await self.data_queue.put(data_item)
53
+ print(f"Data {data_item['id']} added to pending queue")
54
+
55
+ # Simulate interval between data streams
56
+ if i < len(data_list) - 1: # Don't wait after the last item
57
+ await asyncio.sleep(0.8)
58
+
59
+ print("Initial data stream added successfully")
60
+
61
+ async def _process_data_async(self, data_item: dict):
62
+ """Asynchronously process a single data item"""
63
+ data_id = data_item["id"]
64
+ content = data_item["content"]
65
+
66
+ # Simulate different processing times (1-3 seconds)
67
+ processing_time = random.uniform(1, 3)
68
+
69
+ print(f" Starting to process {data_id}, estimated time {processing_time:.1f}s")
70
+
71
+ # Asynchronously wait for processing completion
72
+ await asyncio.sleep(processing_time)
73
+
74
+ result = {
75
+ "id": data_id,
76
+ "processed_content": f"Processed {content}",
77
+ "processing_time": round(processing_time, 2),
78
+ "completed_at": time.time(),
79
+ }
80
+
81
+ # Immediately put into result queue
82
+ await self.result_queue.put(result)
83
+ print(f" {data_id} processing completed! (took {processing_time:.1f}s) -> Added to result queue")
84
+
85
+ async def _submit_worker(self):
86
+ """Stream submission worker coroutine"""
87
+ active_tasks = set()
88
+
89
+ print("Stream submitter started...")
90
+
91
+ while True:
92
+ # Get data to process
93
+ data_item = await self.data_queue.get()
94
+
95
+ if data_item == "DONE":
96
+ print("Received termination signal, waiting for remaining tasks to complete...")
97
+ if active_tasks:
98
+ await asyncio.gather(*active_tasks, return_exceptions=True)
99
+ break
100
+
101
+ # Check concurrent limit
102
+ while len(active_tasks) >= self.max_concurrent_tasks:
103
+ print(f"Reached maximum concurrency {self.max_concurrent_tasks}, waiting for tasks to complete...")
104
+ done_tasks, active_tasks = await asyncio.wait(active_tasks, return_when=asyncio.FIRST_COMPLETED)
105
+
106
+ # Clean up completed tasks
107
+ for task in done_tasks:
108
+ try:
109
+ await task
110
+ print(f"Task completed {task}")
111
+ except Exception as e:
112
+ print(f"Task execution failed: {e}")
113
+
114
+ # Immediately submit new task
115
+ task = asyncio.create_task(self._process_data_async(data_item), name=f"active {data_item}")
116
+ active_tasks.add(task)
117
+
118
+ print(f"Submitted task {data_item['id']}, current concurrency: {len(active_tasks)}")
119
+
120
+ async def _consumer_worker(self):
121
+ """Result consumer coroutine"""
122
+ print("Consumer started...")
123
+
124
+ while True:
125
+ try:
126
+ # Get processing result from result queue
127
+ result = await asyncio.wait_for(self.result_queue.get(), timeout=2.0)
128
+
129
+ self.consumer_count += 1
130
+
131
+ print(
132
+ f"Consumed #{self.consumer_count}: {result['id']} "
133
+ f"(processing time {result['processing_time']}s) - {result['processed_content']}"
134
+ )
135
+
136
+ except asyncio.TimeoutError:
137
+ print(" Consumer waiting...")
138
+ await asyncio.sleep(0.5)
139
+
140
+ async def run_demo(self):
141
+ """Run demonstration"""
142
+ print("=" * 60)
143
+ print(f"Maximum concurrency: {self.max_concurrent_tasks}")
144
+ print("=" * 60)
145
+
146
+ # Start core coroutines
147
+ stream_task = asyncio.create_task(self.data_stream())
148
+ submit_task = asyncio.create_task(self._submit_worker())
149
+ consumer_task = asyncio.create_task(self._consumer_worker())
150
+
151
+ try:
152
+ # Wait for data stream to complete
153
+ await stream_task
154
+ print("Data stream completed")
155
+
156
+ # Wait for processing to complete
157
+ await submit_task
158
+ print("All tasks processed")
159
+
160
+ finally:
161
+ # Cleanup
162
+ submit_task.cancel()
163
+ consumer_task.cancel()
164
+ await asyncio.gather(submit_task, consumer_task, return_exceptions=True)
165
+
166
+ print(f"\nFinal statistics: Consumed {self.consumer_count} results")
167
+
168
+
169
+ async def main():
170
+ """Main function"""
171
+ system = SimpleStreamingSystem(max_concurrent_tasks=3)
172
+ await system.run_demo()
173
+
174
+
175
+ if __name__ == "__main__":
176
+ asyncio.run(main())
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/vllm_rollout/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/vllm_rollout/vllm_async_server.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Meituan Ltd. and/or its affiliates
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import asyncio
15
+ import logging
16
+ from typing import Any, Optional, Sequence
17
+
18
+ import ray
19
+ from ray.actor import ActorHandle
20
+ from vllm import SamplingParams
21
+ from vllm.inputs import TokensPrompt
22
+ from vllm.outputs import RequestOutput
23
+
24
+ from verl.workers.config import HFModelConfig, RolloutConfig
25
+ from verl.workers.rollout.replica import RolloutMode
26
+ from verl.workers.rollout.vllm_rollout.vllm_async_server import (
27
+ _qwen2_5_vl_dedup_image_tokens,
28
+ vLLMHttpServer,
29
+ vLLMReplica,
30
+ )
31
+
32
+ logger = logging.getLogger(__file__)
33
+ logger.setLevel(logging.INFO)
34
+
35
+
36
+ class vLLMHttpServerForPartial(vLLMHttpServer):
37
+ def __init__(
38
+ self,
39
+ config: RolloutConfig,
40
+ model_config: HFModelConfig,
41
+ rollout_mode: RolloutMode,
42
+ workers: list[ActorHandle],
43
+ replica_rank: int,
44
+ node_rank: int,
45
+ gpus_per_node: int,
46
+ nnodes: int,
47
+ ):
48
+ super().__init__(config, model_config, rollout_mode, workers, replica_rank, node_rank, gpus_per_node, nnodes)
49
+
50
+ # for cancel LLMServer
51
+ self.paused = False
52
+ self.lock = asyncio.Lock()
53
+ self.cancel_event: dict[str, asyncio.Event] = {}
54
+ self.req_output: dict[str, Optional[RequestOutput]] = {}
55
+
56
+ async def _generate_step(
57
+ self,
58
+ prompt_ids: list[int],
59
+ sampling_params: dict[str, Any],
60
+ request_id: str,
61
+ image_data: Optional[list[Any]] = None,
62
+ ):
63
+ max_tokens = self.config.max_model_len - len(prompt_ids)
64
+ sampling_params["logprobs"] = 1
65
+ sampling_params.setdefault("repetition_penalty", self.config.get("repetition_penalty", 1.0))
66
+ sampling_params = SamplingParams(max_tokens=max_tokens, **sampling_params)
67
+ prompt_ids = _qwen2_5_vl_dedup_image_tokens(prompt_ids, self.model_config.processor)
68
+ prompt = TokensPrompt(
69
+ prompt_token_ids=prompt_ids, multi_modal_data={"image": image_data} if image_data else None
70
+ )
71
+ generator = self.engine.generate(prompt=prompt, sampling_params=sampling_params, request_id=request_id)
72
+
73
+ # Get final response
74
+ async for output in generator:
75
+ self.req_output[request_id] = output
76
+ assert self.req_output[request_id] is not None
77
+
78
+ async def generate_for_partial(
79
+ self,
80
+ prompt_ids: list[int],
81
+ sampling_params: dict[str, Any],
82
+ request_id: str,
83
+ image_data: Optional[list[Any]] = None,
84
+ ) -> tuple[list[Any], list[Any], bool] | tuple[Sequence[int], list[float], Any]:
85
+ async with self.lock:
86
+ if self.paused:
87
+ # After cancel, all tasks will return directly and wait for the next submission
88
+ return [], [], True
89
+ self.req_output[request_id]: Optional[RequestOutput] = None
90
+ self.cancel_event[request_id] = asyncio.Event()
91
+ cancel_handle = asyncio.create_task(self.cancel_event[request_id].wait())
92
+ generation_handle = asyncio.create_task(
93
+ self._generate_step(prompt_ids, sampling_params, request_id, image_data)
94
+ )
95
+
96
+ done, pend = await asyncio.wait([generation_handle, cancel_handle], return_when=asyncio.FIRST_COMPLETED)
97
+
98
+ for task in done:
99
+ await task
100
+
101
+ for task in pend:
102
+ task.cancel()
103
+
104
+ async with self.lock:
105
+ if self.req_output[request_id] is None:
106
+ return [], [], True
107
+ token_ids = self.req_output[request_id].outputs[0].token_ids
108
+ log_probs: list[float] = []
109
+ for i, x in enumerate(self.req_output[request_id].outputs[0].logprobs):
110
+ # In sampling_params, logprobs is set to 1, which should return 1,
111
+ # but in practice there are multiple. Take the log_prob corresponding to token_id
112
+ token_id = self.req_output[request_id].outputs[0].token_ids[i]
113
+ log_probs.append(x[token_id].logprob)
114
+ is_cancel = generation_handle not in done
115
+ self.cancel_event.pop(request_id, None)
116
+ self.req_output.pop(request_id, None)
117
+ return token_ids, log_probs, is_cancel
118
+
119
+ async def cancel(self):
120
+ async with self.lock:
121
+ self.paused = True
122
+ for request_id in self.cancel_event:
123
+ self.cancel_event[request_id].set()
124
+
125
+ async def resume(self):
126
+ async with self.lock:
127
+ self.paused = False
128
+
129
+
130
+ class FullyAsyncvLLMReplica(vLLMReplica):
131
+ def __init__(
132
+ self,
133
+ replica_rank: int,
134
+ config: RolloutConfig,
135
+ model_config: HFModelConfig,
136
+ gpus_per_node: int = 8,
137
+ is_reward_model: bool = False,
138
+ ):
139
+ super().__init__(replica_rank, config, model_config, gpus_per_node, is_reward_model)
140
+ self.server_class = ray.remote(vLLMHttpServerForPartial)
141
+
142
+ async def cancel(self):
143
+ """Cancel each rollout server."""
144
+ await asyncio.gather(*[server.cancel.remote() for server in self.servers])
145
+
146
+ async def resume(self):
147
+ """Resume each rollout server."""
148
+ await asyncio.gather(*[server.resume.remote() for server in self.servers])