Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/__init__.py +21 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/agent_loop.py +1022 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/prometheus_utils.py +110 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/single_turn_agent_loop.py +84 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_agent_loop.py +475 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/tool_parser.py +161 -0
- code/RL_model/verl/verl_train/verl/experimental/agent_loop/utils.py +108 -0
- code/RL_model/verl/verl_train/verl/experimental/dataset/__init__.py +13 -0
- code/RL_model/verl/verl_train/verl/experimental/dataset/sampler.py +40 -0
- code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/__init__.py +13 -0
- code/RL_model/verl/verl_train/verl/experimental/dynamic_dataset/dynamicgen_dataset.py +112 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README.md +599 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/README_zh.md +517 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/__init__.py +20 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/agent_loop.py +370 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_single_turn_agent_loop.py +115 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/agent_loop/partial_tool_agent_loop.py +281 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/base_detach_sync.py +238 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/checkpoint_engine.py +522 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_megatron_trainer.yaml +76 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/config/fully_async_ppo_trainer.yaml +76 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/detach_utils.py +363 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp2_utils.py +125 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fsdp_workers.py +247 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_main.py +312 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_rollouter.py +793 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/fully_async_trainer.py +612 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_utils.py +99 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/megatron_worker.py +267 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/message_queue.py +265 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/param_sync.py +173 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/ray_trainer.py +538 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/__init__.py +13 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/sglang_rollout/sglang_async_server.py +189 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_30b_a3b_base_math_fsdp.sh +191 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_async_retool.sh +141 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_16_16.sh +162 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_32_32.sh +162 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_12.sh +164 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_4_4.sh +164 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64.sh +162 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_64_64_mis.sh +173 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/dapo_7b_math_fsdp2_8_8.sh +162 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/geo3k_qwen25vl_7b_megatron_4_4.sh +111 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32.sh +230 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/grpo_30b_a3b_base_math_megatron_96_32_mis.sh +239 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/shell/runtime_env.yaml +4 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/unittest/simple_streaming_demo.py +176 -0
- code/RL_model/verl/verl_train/verl/experimental/fully_async_policy/vllm_rollout/__init__.py +13 -0
- 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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])
|